Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fe38b9210 | ||
|
|
c953c3f4c8 | ||
|
|
1a71de0983 | ||
|
|
b0494f6eb4 | ||
|
|
fe9e9c7ba3 | ||
|
|
d41dc9a632 | ||
|
|
4c659a1d82 | ||
|
|
b56f21b87d | ||
|
|
4d49a7c228 | ||
|
|
d8569fbd1f | ||
|
|
ed7d153cf6 | ||
|
|
a3c0be4f40 | ||
|
|
c31f25c51e | ||
|
|
5367eed3b6 | ||
|
|
1586d774df | ||
|
|
0aeeccd42a | ||
|
|
4a807e41e8 | ||
|
|
05cee23bbb | ||
|
|
bbb4f1c53f | ||
|
|
f74d52369f | ||
|
|
419873a3c6 | ||
|
|
357a03aa8f | ||
|
|
164dbed673 | ||
|
|
6ccb6be3a0 | ||
|
|
7cba594046 | ||
|
|
d48cd13d53 | ||
|
|
ae83544c3a | ||
|
|
a996b54323 | ||
|
|
91317dcba3 | ||
|
|
e6ac1cc802 | ||
|
|
3649e3994a | ||
|
|
dd7d28c44f | ||
|
|
b58a0e531f | ||
|
|
2b4629b505 | ||
|
|
698b8dbb97 | ||
|
|
a4bc285cb7 | ||
|
|
7c2f616dfe | ||
|
|
4d0e00a7c4 | ||
|
|
e3220d4a8d | ||
|
|
68e0aa2d90 | ||
|
|
ce00999e95 | ||
|
|
347c4de046 | ||
|
|
48eb472645 | ||
|
|
6530e41948 | ||
|
|
06bc0c1ed4 | ||
|
|
ccc3a2d2c8 | ||
|
|
deb704a084 | ||
|
|
dc86fa2e6e | ||
|
|
64d1b79b61 | ||
|
|
f4e84e099a | ||
|
|
b9807f001b | ||
|
|
358a7577c1 | ||
|
|
c05a94d985 | ||
|
|
ab5d6bcfe2 | ||
|
|
d7e59305bd | ||
|
|
edb86b9728 | ||
|
|
23c1cfa283 | ||
|
|
9944f9d731 | ||
|
|
2e3c5e9dbd | ||
|
|
206b825d7c | ||
|
|
584aa7a5fe | ||
|
|
2500be36e7 | ||
|
|
b7daefb394 | ||
|
|
9eeb69e3dd | ||
|
|
2e963137fb | ||
|
|
0ae539f384 | ||
|
|
dce096fba4 | ||
|
|
34e6f69568 | ||
|
|
c9e14e366c | ||
|
|
4308c4a1cf | ||
|
|
65eb299eb0 | ||
|
|
1309602889 | ||
|
|
09ebee8e41 | ||
|
|
ea2247c40c | ||
|
|
a8061277fb | ||
|
|
eb94d2ad18 | ||
|
|
b3aabee23b | ||
|
|
72fad8f22d | ||
|
|
e2db196790 | ||
|
|
ae28e27a8c | ||
|
|
9640b184ad | ||
|
|
a2e13820f2 | ||
|
|
d0ea16e887 | ||
|
|
1f3448e1ea | ||
|
|
ad619a779c | ||
|
|
09e89e5e6b | ||
|
|
baefa5e9c7 | ||
|
|
57e87528c8 | ||
|
|
0b5dc2a137 | ||
|
|
0d23d5d6d5 | ||
|
|
16376e56d7 | ||
|
|
3f923e4469 | ||
|
|
c7a417e285 | ||
|
|
a97d7636ba | ||
|
|
80e0957857 | ||
|
|
14d3d18ad2 | ||
|
|
079dc921d4 | ||
|
|
c2e86c6828 | ||
|
|
9d519c6590 | ||
|
|
d04b03226a | ||
|
|
c24dadaa60 | ||
|
|
2c751b4fce | ||
|
|
e7f7ae8452 | ||
|
|
9c292a4903 | ||
|
|
b06272c35d | ||
|
|
366646e354 | ||
|
|
8eeec64388 | ||
|
|
4dea4ba3c0 | ||
|
|
a66071b2c2 | ||
|
|
306f3da606 | ||
|
|
e6495c1541 | ||
|
|
825ef239a0 | ||
|
|
13d9741f77 | ||
|
|
94f5ed601d | ||
|
|
4463848671 | ||
|
|
3b49b18f4d | ||
|
|
2e0ca0ecee | ||
|
|
ff6a319caf | ||
|
|
744c570ed1 | ||
|
|
fff9550c8f | ||
|
|
f925451c15 | ||
|
|
fd30702ffd | ||
|
|
6cd279bc49 | ||
|
|
75cc5a6434 | ||
|
|
e7d6161819 | ||
|
|
4171f1536d | ||
|
|
5eb495581d | ||
|
|
36172a0476 | ||
|
|
cee989d41d | ||
|
|
c55652f310 | ||
|
|
9246ecb1e6 | ||
|
|
337c8c953e | ||
|
|
7e21d93a77 | ||
|
|
97f7ad4d29 | ||
|
|
f702cf7216 | ||
|
|
0797a56ba6 | ||
|
|
76d35bd428 | ||
|
|
24459b259b |
@@ -20,6 +20,7 @@
|
||||
"playground-web",
|
||||
"thirdweb-dashboard",
|
||||
"wallet-ui",
|
||||
"portal",
|
||||
"@thirdweb-dev/auth",
|
||||
"@thirdweb-dev/chains",
|
||||
"@thirdweb-dev/cli",
|
||||
|
||||
@@ -5,51 +5,52 @@
|
||||
|
||||
# legacy packages
|
||||
|
||||
legacy_packages/auth/ @jnsdls
|
||||
legacy_packages/chains/ @jnsdls
|
||||
legacy_packages/cli/ @jnsdls
|
||||
legacy_packages/contracts-js/ @jnsdls
|
||||
legacy_packages/crypto/ @jnsdls
|
||||
legacy_packages/generated-abis/ @jnsdls
|
||||
legacy_packages/merkletree/ @jnsdls
|
||||
legacy_packages/pay/ @jnsdls
|
||||
legacy_packages/payments/ @jnsdls
|
||||
legacy_packages/react/ @MananTank @jnsdls
|
||||
legacy_packages/react-core/ @MananTank @jnsdls
|
||||
legacy_packages/react-native/ @jnsdls
|
||||
legacy_packages/react-native-compat/ @jnsdls
|
||||
legacy_packages/auth/ @jnsdls @joaquim-verges
|
||||
legacy_packages/chains/ @jnsdls @joaquim-verges
|
||||
legacy_packages/cli/ @jnsdls @joaquim-verges
|
||||
legacy_packages/contracts-js/ @jnsdls @joaquim-verges
|
||||
legacy_packages/crypto/ @jnsdls @joaquim-verges
|
||||
legacy_packages/generated-abis/ @jnsdls @joaquim-verges
|
||||
legacy_packages/merkletree/ @jnsdls @joaquim-verges
|
||||
legacy_packages/pay/ @jnsdls @joaquim-verges
|
||||
legacy_packages/payments/ @jnsdls @joaquim-verges
|
||||
legacy_packages/react/ @MananTank @jnsdls @joaquim-verges
|
||||
legacy_packages/react-core/ @MananTank @jnsdls @joaquim-verges
|
||||
legacy_packages/react-native/ @jnsdls @joaquim-verges
|
||||
legacy_packages/react-native-compat/ @jnsdls @joaquim-verges
|
||||
legacy_packages/sdk/ @joaquim-verges @jnsdls
|
||||
legacy_packages/storage/ @jnsdls
|
||||
legacy_packages/wallets/ @MananTank @jnsdls
|
||||
legacy_packages/storage/ @jnsdls @joaquim-verges
|
||||
legacy_packages/wallets/ @MananTank @jnsdls @joaquim-verges
|
||||
|
||||
# legacy e2e
|
||||
|
||||
legacy_e2e/ @jnsdls
|
||||
|
||||
# packages
|
||||
packages/thirdweb/ @joaquim-verges @gregfromstl
|
||||
packages/thirdweb/ @joaquim-verges @gregfromstl @jnsdls
|
||||
|
||||
## specific thirdweb pieces
|
||||
packages/thirdweb/src/react/ @joaquim-verges @gregfromstl @MananTank
|
||||
packages/thirdweb/src/reactive/ @joaquim-verges @gregfromstl @MananTank
|
||||
packages/thirdweb/src/gas/ @joaquim-verges
|
||||
packages/thirdweb/src/pay/ @joaquim-verges @gregfromstl @MananTank
|
||||
packages/typedoc-gen/ @MananTank
|
||||
packages/service-utils/ @arcoraven
|
||||
packages/eslint-config-thirdweb/ @jnsdls
|
||||
packages/tw-tsconfig/ @jnsdls
|
||||
packages/unity-js-bridge/ @joaquim-verges @0xFirekeeper
|
||||
packages/thirdweb/src/react/ @joaquim-verges @gregfromstl @MananTank @jnsdls
|
||||
packages/thirdweb/src/reactive/ @joaquim-verges @gregfromstl @MananTank @jnsdls
|
||||
packages/thirdweb/src/gas/ @joaquim-verges @jnsdls
|
||||
packages/thirdweb/src/pay/ @joaquim-verges @gregfromstl @MananTank @jnsdls
|
||||
packages/typedoc-gen/ @MananTank @jnsdls @joaquim-verges
|
||||
packages/service-utils/ @arcoraven @jnsdls @joaquim-verges
|
||||
packages/eslint-config-thirdweb/ @jnsdls @joaquim-verges
|
||||
packages/tw-tsconfig/ @jnsdls @joaquim-verges
|
||||
packages/unity-js-bridge/ @joaquim-verges @0xFirekeeper @jnsdls
|
||||
|
||||
# apps
|
||||
apps/ @jnsdls
|
||||
apps/dashboard/ @jnsdls @MananTank
|
||||
apps/playground-web/ @joaquim-verges @gregfromstl
|
||||
apps/wallet-ui/ @gregfromstl
|
||||
apps/ @jnsdls @joaquim-verges
|
||||
apps/dashboard/ @jnsdls @MananTank @joaquim-verges
|
||||
apps/playground-web/ @joaquim-verges @gregfromstl @jnsdls @joaquim-verges
|
||||
apps/wallet-ui/ @gregfromstl @jnsdls @joaquim-verges @MananTank
|
||||
apps/portal/ @gregfromstl @jnsdls @joaquim-verges @MananTank
|
||||
|
||||
# .github folder + .changeset + turbo config is owned by jonas for now
|
||||
scripts/ @jnsdls
|
||||
.github/ @jnsdls
|
||||
.changeset/config.json @jnsdls
|
||||
turbo.json @jnsdls
|
||||
.vscode/ @jnsdls
|
||||
scripts/ @jnsdls @joaquim-verges
|
||||
.github/ @jnsdls @joaquim-verges
|
||||
.changeset/config.json @jnsdls @joaquim-verges
|
||||
turbo.json @jnsdls @joaquim-verges
|
||||
.vscode/ @jnsdls @joaquim-verges
|
||||
|
||||
|
||||
@@ -77,6 +77,21 @@ Many of the tests use forked versions of live chains like mainnet. We fork these
|
||||
|
||||
If you need to use accounts in your tests, use the predefined accounts in `test/src/test-wallets.ts`. These are the default anvil accounts that are pre-funded on the local test forks.
|
||||
|
||||
##### Mocking Network Calls
|
||||
|
||||
If your test depends on a downstream network call, you must mock the call using `msw`. You can use one of the existing mocks in `test/src/mocks` or create your own. Once you've created a mock request, setup the server and add all your mocks like so:
|
||||
|
||||
```ts
|
||||
import { setupServer } from "msw/node";
|
||||
import { downloadMock, uploadMock } from "../../../test/src/mocks/storage.js";
|
||||
|
||||
const server = setupServer(uploadMock("HASH"), downloadMock({ name: "Test NFT" }));
|
||||
|
||||
beforeAll(() => server.listen());
|
||||
afterEach(() => server.resetHandlers());
|
||||
afterAll(() => server.close());
|
||||
```
|
||||
|
||||
#### Linting
|
||||
|
||||
We use a linter to maintain best practices across projects. Once your changes are complete (or periodically while making changes), run the linter with the following command from the repo root:
|
||||
|
||||
@@ -11,3 +11,6 @@ legacy_packages:
|
||||
packages:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "packages/**/*"
|
||||
portal:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "apps/portal/**/*"
|
||||
|
||||
@@ -2,18 +2,3 @@
|
||||
|
||||
Short description of the bug fixed or feature added
|
||||
|
||||
## Changes made
|
||||
|
||||
- [ ] Public API changes: list the public API changes made if any
|
||||
- [ ] Internal API changes: explain the internal logic changes
|
||||
|
||||
## How to test
|
||||
|
||||
- [ ] Automated tests: link to unit test file
|
||||
- [ ] Manual tests: step by step instructions on how to test
|
||||
|
||||
## Contributor NFT
|
||||
|
||||
Paste in your wallet address below and we will airdrop you a special NFT when your pull request is merged.
|
||||
|
||||
```Address: ```
|
||||
|
||||
@@ -142,31 +142,6 @@ jobs:
|
||||
echo "Bundling failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
benchmark:
|
||||
# cannot run benchmarks on merge group
|
||||
if: github.event_name != 'merge_group'
|
||||
timeout-minutes: 15
|
||||
name: "Benchmarks"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup & Install
|
||||
uses: ./.github/composite-actions/install
|
||||
|
||||
- name: Set up foundry
|
||||
uses: foundry-rs/foundry-toolchain@v1
|
||||
with:
|
||||
cache: false
|
||||
|
||||
- name: Run benchmarks
|
||||
uses: CodSpeedHQ/action@v3
|
||||
with:
|
||||
token: ${{ secrets.CODSPEED_TOKEN }}
|
||||
run: "pnpm bench"
|
||||
|
||||
size:
|
||||
# only run on pull requests
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
# On the Version Packages branch
|
||||
# Generate and Commit the Typedoc JSON files and the Snippets JSON files
|
||||
|
||||
name: Commit Release Artifacts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- changeset-release/main
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
# cancel any previous runs if they were started before this one
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
|
||||
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
|
||||
TW_SECRET_KEY: ${{ secrets.TW_SECRET_KEY }}
|
||||
|
||||
jobs:
|
||||
release-artifacts:
|
||||
timeout-minutes: 60
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# bun setup is required, because pnpm typdoc will run pnpm build and some packages uses bun for build
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
with:
|
||||
bun-version: 1.0.35
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/composite-actions/install
|
||||
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
|
||||
- name: Typedoc
|
||||
run: pnpm typedoc
|
||||
|
||||
- name: Generate Docs
|
||||
run: |
|
||||
cd legacy_packages/sdk
|
||||
pnpm generate-docs
|
||||
cd ../react-core
|
||||
pnpm generate-docs
|
||||
cd ../react
|
||||
pnpm generate-docs
|
||||
cd ../..
|
||||
|
||||
- name: Generate Snippets
|
||||
run: pnpm generate-snippets
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
git config --local user.email "[email protected]"
|
||||
git config --local user.name "GitHub Action"
|
||||
git status
|
||||
git add legacy_packages/react/typedoc/documentation.json.gz
|
||||
git add legacy_packages/react-core/typedoc/documentation.json.gz
|
||||
git add legacy_packages/react-native/typedoc/documentation.json.gz
|
||||
git add legacy_packages/sdk/typedoc/documentation.json.gz
|
||||
git add legacy_packages/storage/typedoc/documentation.json.gz
|
||||
git add legacy_packages/wallets/typedoc/documentation.json.gz
|
||||
git add packages/thirdweb/typedoc/documentation.json.gz
|
||||
git add snippets/feature_snippets_react.json
|
||||
git add snippets/feature_snippets_sdk.json
|
||||
git add snippets/snippets.json
|
||||
git commit -m "Commit Release Artifacts [skip ci]"
|
||||
git push
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -14,7 +14,7 @@ public/dist
|
||||
.idea/
|
||||
.yalc/
|
||||
yalc.lock
|
||||
build/
|
||||
./build/
|
||||
playwright-report/
|
||||
.env/
|
||||
|
||||
@@ -25,4 +25,8 @@ legacy_packages/cli/artifacts/
|
||||
codecov
|
||||
|
||||
# Remove sitemap files (generated at build time)
|
||||
sitemap*.xml
|
||||
sitemap*.xml
|
||||
|
||||
# typedoc
|
||||
legacy_packages/*/typedoc/*
|
||||
packages/*/typedoc/*
|
||||
|
||||
@@ -15,6 +15,21 @@ module.exports = {
|
||||
message:
|
||||
'Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery".',
|
||||
},
|
||||
{
|
||||
selector: "CallExpression[callee.name='defineChain']",
|
||||
message:
|
||||
"Use useV5DashboardChain instead if you are using it inside a component",
|
||||
},
|
||||
{
|
||||
selector: "CallExpression[callee.name='defineDashboardChain']",
|
||||
message:
|
||||
"Use useV5DashboardChain instead if you are using it inside a component",
|
||||
},
|
||||
{
|
||||
selector: "CallExpression[callee.name='mapV4ChainToV5Chain']",
|
||||
message:
|
||||
"Use useV5DashboardChain instead if you are using it inside a component",
|
||||
},
|
||||
],
|
||||
"no-restricted-imports": [
|
||||
"error",
|
||||
|
||||
@@ -52,11 +52,9 @@
|
||||
"@tanstack/react-query-persist-client": "^4.36.1",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"@thirdweb-dev/react": "workspace:*",
|
||||
"@thirdweb-dev/react-core": "workspace:*",
|
||||
"@thirdweb-dev/sdk": "workspace:*",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@thirdweb-dev/storage": "workspace:*",
|
||||
"@thirdweb-dev/wallets": "workspace:*",
|
||||
"@vercel/og": "^0.6.2",
|
||||
"abitype": "1.0.5",
|
||||
"chakra-react-select": "^4.7.6",
|
||||
@@ -106,13 +104,13 @@
|
||||
"server-only": "^0.0.1",
|
||||
"sonner": "^1.5.0",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"spin-wheel-game": "1.0.3-alpha.3",
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwind-merge": "^2.4.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-debounce": "^10.0.1",
|
||||
"vaul": "^0.9.1",
|
||||
"zksync-ethers": "5.9.0",
|
||||
"zod": "3.23.8"
|
||||
},
|
||||
@@ -150,7 +148,7 @@
|
||||
"next-sitemap": "^4.2.3",
|
||||
"next-unused": "^0.0.6",
|
||||
"tailwindcss": "^3.4.4",
|
||||
"typescript": "5.5.2"
|
||||
"typescript": "5.5.4"
|
||||
},
|
||||
"next-unused": {
|
||||
"alias": {},
|
||||
|
||||
@@ -275,6 +275,13 @@ function redirects() {
|
||||
destination: "/dashboard/connect/ecosystem/:slug/permissions",
|
||||
permanent: false,
|
||||
},
|
||||
// temporarily redirect cli login to support page
|
||||
{
|
||||
source: "/cli/login",
|
||||
destination:
|
||||
"https://support.thirdweb.com/troubleshooting-errors/7Y1BqKNvtLdBv5fZkRZZB3/issue-linking-device-on-the-authorization-page-via-thirdweb-cli/cn9LRA3ax7XCP6uxwRYdvx",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { MenuIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
|
||||
export function BurgerMenuButton(props: {
|
||||
links?: { href: string; label: React.ReactNode }[];
|
||||
footer?: React.ReactNode;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button size="icon" variant="outline">
|
||||
<MenuIcon strokeWidth={1} />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="p-6 rounded-xl"
|
||||
dialogCloseClassName="hidden"
|
||||
onClick={(e) => {
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{props.links?.map((link) => {
|
||||
return (
|
||||
<Button size="sm" variant="ghost" asChild key={link.href}>
|
||||
<NavLink
|
||||
href={link.href}
|
||||
className="!text-left justify-start flex gap-2 h-auto py-3"
|
||||
activeClassName="bg-accent"
|
||||
>
|
||||
{link.label}
|
||||
</NavLink>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{props.footer}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
|
||||
export type SidebarContentProps = {
|
||||
header?: React.ReactNode;
|
||||
links?: { href: string; label: React.ReactNode }[];
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Sidebar(props: SidebarContentProps) {
|
||||
return (
|
||||
<aside
|
||||
className={cn("w-[240px] flex-shrink-0 hidden lg:block", props.className)}
|
||||
>
|
||||
<div className="sticky top-0 pt-8">
|
||||
{props.header}
|
||||
<div className="flex flex-col border-l-2 border-border">
|
||||
{props.links?.map((link) => (
|
||||
<NavLink
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="text-sm py-1.5 pl-4 text-muted-foreground hover:text-foreground border-l-2 ml-[-2px] border-transparent hover:border-foreground transition-colors"
|
||||
activeClassName="text-foreground border-link-foreground"
|
||||
>
|
||||
{link.label}
|
||||
</NavLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "app/components/ThirdwebMiniLogo";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
SiDiscord,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
SiX,
|
||||
SiYoutube,
|
||||
} from "react-icons/si";
|
||||
import { ThirdwebMiniLogo } from "../../../app/components/ThirdwebMiniLogo";
|
||||
|
||||
type AppFooterProps = {
|
||||
className?: string;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ClientOnly } from "components/ClientOnly/ClientOnly";
|
||||
import { Moon, Sun } from "lucide-react";
|
||||
import { useTheme } from "next-themes";
|
||||
import * as React from "react";
|
||||
import { ClientOnly } from "../../components/ClientOnly/ClientOnly";
|
||||
import { Skeleton } from "./ui/skeleton";
|
||||
|
||||
export function ColorModeToggle() {
|
||||
|
||||
@@ -1,9 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||
import { ThemeProvider as NextThemesProvider, useTheme } from "next-themes";
|
||||
import type { ThemeProviderProps } from "next-themes/dist/types";
|
||||
import * as React from "react";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||
}
|
||||
|
||||
export function useForceDarkTheme() {
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
setTheme("dark");
|
||||
}, [setTheme]);
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ import {
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import type { buttonVariants } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import type React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export interface ConfirmationDialogProps
|
||||
extends VariantProps<typeof buttonVariants> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "../../lib/utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CopyTextButton } from "./CopyTextButton";
|
||||
|
||||
export function CopyAddressButton(props: {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "./button";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "./button";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
@@ -23,10 +23,15 @@ export function CopyTextButton(props: {
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyButton = isCopied ? (
|
||||
<CheckIcon className={cn("size-3 text-green-500", props.iconClassName)} />
|
||||
<CheckIcon
|
||||
className={cn("size-3 text-green-500 shrink-0", props.iconClassName)}
|
||||
/>
|
||||
) : (
|
||||
<CopyIcon
|
||||
className={cn("size-3 text-muted-foreground", props.iconClassName)}
|
||||
className={cn(
|
||||
"size-3 text-muted-foreground shrink-0",
|
||||
props.iconClassName,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -36,7 +41,7 @@ export function CopyTextButton(props: {
|
||||
variant={props.variant || "outline"}
|
||||
aria-label={props.tooltip}
|
||||
className={cn(
|
||||
"h-auto w-auto px-2.5 py-1.5 flex gap-2 rounded-lg text-foreground",
|
||||
"h-auto w-auto px-2.5 py-1.5 flex gap-2 rounded-lg text-foreground font-normal",
|
||||
props.className,
|
||||
)}
|
||||
onClick={() => {
|
||||
@@ -47,13 +52,13 @@ export function CopyTextButton(props: {
|
||||
>
|
||||
{props.copyIconPosition === "right" ? (
|
||||
<>
|
||||
{props.textToShow}
|
||||
<span className="truncate min-w-0"> {props.textToShow} </span>
|
||||
{copyButton}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{copyButton}
|
||||
{props.textToShow}
|
||||
<span className="truncate min-w-0"> {props.textToShow} </span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export type NavButtonProps = {
|
||||
className?: string;
|
||||
activeClassName?: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
|
||||
const pathname = usePathname();
|
||||
const isActive = pathname === props.href;
|
||||
return (
|
||||
<Link
|
||||
href={props.href}
|
||||
className={cn(props.className, isActive && props.activeClassName)}
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -44,10 +44,13 @@ export interface ButtonProps
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const btnOnlyProps =
|
||||
Comp === "button" ? { type: "button" as const } : undefined;
|
||||
return (
|
||||
<Comp
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...btnOnlyProps}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import * as RechartsPrimitive from "recharts";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
// Format: { THEME_NAME: CSS_SELECTOR }
|
||||
const THEMES = { light: "", dark: ".dark" } as const;
|
||||
|
||||
export type ChartConfig = {
|
||||
[k in string]: {
|
||||
label?: React.ReactNode;
|
||||
icon?: React.ComponentType;
|
||||
} & (
|
||||
| { color?: string; theme?: never }
|
||||
| { color?: never; theme: Record<keyof typeof THEMES, string> }
|
||||
);
|
||||
};
|
||||
|
||||
type ChartContextProps = {
|
||||
config: ChartConfig;
|
||||
};
|
||||
|
||||
const ChartContext = React.createContext<ChartContextProps | null>(null);
|
||||
|
||||
function useChart() {
|
||||
const context = React.useContext(ChartContext);
|
||||
|
||||
if (!context) {
|
||||
throw new Error("useChart must be used within a <ChartContainer />");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const ChartContainer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
config: ChartConfig;
|
||||
children: React.ComponentProps<
|
||||
typeof RechartsPrimitive.ResponsiveContainer
|
||||
>["children"];
|
||||
}
|
||||
>(({ id, className, children, config, ...props }, ref) => {
|
||||
const uniqueId = React.useId();
|
||||
const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`;
|
||||
|
||||
return (
|
||||
<ChartContext.Provider value={{ config }}>
|
||||
<div
|
||||
data-chart={chartId}
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChartStyle id={chartId} config={config} />
|
||||
<RechartsPrimitive.ResponsiveContainer>
|
||||
{children}
|
||||
</RechartsPrimitive.ResponsiveContainer>
|
||||
</div>
|
||||
</ChartContext.Provider>
|
||||
);
|
||||
});
|
||||
ChartContainer.displayName = "Chart";
|
||||
|
||||
const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
|
||||
const colorConfig = Object.entries(config).filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
([_, config]) => config.theme || config.color,
|
||||
);
|
||||
|
||||
if (!colorConfig.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<style
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: Object.entries(THEMES)
|
||||
.map(
|
||||
([theme, prefix]) => `
|
||||
${prefix} [data-chart=${id}] {
|
||||
${colorConfig
|
||||
.map(([key, itemConfig]) => {
|
||||
const color =
|
||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
||||
itemConfig.color;
|
||||
return color ? ` --color-${key}: ${color};` : null;
|
||||
})
|
||||
.join("\n")}
|
||||
}
|
||||
`,
|
||||
)
|
||||
.join("\n"),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ChartTooltip = RechartsPrimitive.Tooltip;
|
||||
|
||||
const ChartTooltipContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
||||
React.ComponentProps<"div"> & {
|
||||
hideLabel?: boolean;
|
||||
hideIndicator?: boolean;
|
||||
indicator?: "line" | "dot" | "dashed";
|
||||
nameKey?: string;
|
||||
labelKey?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
active,
|
||||
payload,
|
||||
className,
|
||||
indicator = "dot",
|
||||
hideLabel = false,
|
||||
hideIndicator = false,
|
||||
label,
|
||||
labelFormatter,
|
||||
labelClassName,
|
||||
formatter,
|
||||
color,
|
||||
nameKey,
|
||||
labelKey,
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
|
||||
const tooltipLabel = React.useMemo(() => {
|
||||
if (hideLabel || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [item] = payload;
|
||||
const key = `${labelKey || item.dataKey || item.name || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const value =
|
||||
!labelKey && typeof label === "string"
|
||||
? config[label as keyof typeof config]?.label || label
|
||||
: itemConfig?.label;
|
||||
|
||||
if (labelFormatter) {
|
||||
return (
|
||||
<div className={cn("font-medium", labelClassName)}>
|
||||
{labelFormatter(value, payload)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>;
|
||||
}, [
|
||||
label,
|
||||
labelFormatter,
|
||||
payload,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
config,
|
||||
labelKey,
|
||||
]);
|
||||
|
||||
if (!active || !payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const nestLabel = payload.length === 1 && indicator !== "dot";
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{!nestLabel ? tooltipLabel : null}
|
||||
<div className="grid gap-1.5">
|
||||
{payload.map((item, index) => {
|
||||
const key = `${nameKey || item.name || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
const indicatorColor = color || item.payload.fill || item.color;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.dataKey}
|
||||
className={cn(
|
||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
||||
indicator === "dot" && "items-center",
|
||||
)}
|
||||
>
|
||||
{formatter && item?.value !== undefined && item.name ? (
|
||||
formatter(item.value, item.name, item, index, item.payload)
|
||||
) : (
|
||||
<>
|
||||
{/* Icon */}
|
||||
{itemConfig?.icon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
!hideIndicator && (
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
||||
{
|
||||
"h-2.5 w-2.5": indicator === "dot",
|
||||
"w-1": indicator === "line",
|
||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
||||
indicator === "dashed",
|
||||
"my-0.5": nestLabel && indicator === "dashed",
|
||||
},
|
||||
)}
|
||||
style={
|
||||
{
|
||||
"--color-bg": indicatorColor,
|
||||
"--color-border": indicatorColor,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 gap-2 justify-between leading-none",
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
)}
|
||||
>
|
||||
<div className="grid gap-1.5">
|
||||
{nestLabel ? tooltipLabel : null}
|
||||
<span className="text-muted-foreground">
|
||||
{itemConfig?.label || item.name}
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
{item.value}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartTooltipContent.displayName = "ChartTooltip";
|
||||
|
||||
const ChartLegend = RechartsPrimitive.Legend;
|
||||
|
||||
const ChartLegendContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> &
|
||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
||||
hideIcon?: boolean;
|
||||
nameKey?: string;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
||||
ref,
|
||||
) => {
|
||||
const { config } = useChart();
|
||||
|
||||
if (!payload?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-4",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{payload.map((item) => {
|
||||
const key = `${nameKey || item.dataKey || "value"}`;
|
||||
const itemConfig = getPayloadConfigFromPayload(config, item, key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
{itemConfig?.icon && !hideIcon ? (
|
||||
<itemConfig.icon />
|
||||
) : (
|
||||
<div
|
||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
||||
style={{
|
||||
backgroundColor: item.color,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{itemConfig?.label}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
ChartLegendContent.displayName = "ChartLegend";
|
||||
|
||||
// Helper to extract item config from a payload.
|
||||
function getPayloadConfigFromPayload(
|
||||
config: ChartConfig,
|
||||
payload: unknown,
|
||||
key: string,
|
||||
) {
|
||||
if (typeof payload !== "object" || payload === null) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const payloadPayload =
|
||||
"payload" in payload &&
|
||||
typeof payload.payload === "object" &&
|
||||
payload.payload !== null
|
||||
? payload.payload
|
||||
: undefined;
|
||||
|
||||
let configLabelKey: string = key;
|
||||
|
||||
if (
|
||||
key in payload &&
|
||||
typeof payload[key as keyof typeof payload] === "string"
|
||||
) {
|
||||
configLabelKey = payload[key as keyof typeof payload] as string;
|
||||
} else if (
|
||||
payloadPayload &&
|
||||
key in payloadPayload &&
|
||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
||||
) {
|
||||
configLabelKey = payloadPayload[
|
||||
key as keyof typeof payloadPayload
|
||||
] as string;
|
||||
}
|
||||
|
||||
return configLabelKey in config
|
||||
? config[configLabelKey]
|
||||
: config[key as keyof typeof config];
|
||||
}
|
||||
|
||||
export {
|
||||
ChartContainer,
|
||||
ChartTooltip,
|
||||
ChartTooltipContent,
|
||||
ChartLegend,
|
||||
ChartLegendContent,
|
||||
ChartStyle,
|
||||
};
|
||||
@@ -28,3 +28,19 @@ const Checkbox = React.forwardRef<
|
||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||
|
||||
export { Checkbox };
|
||||
|
||||
export function CheckboxWithLabel(props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<label
|
||||
className={cn(
|
||||
"flex gap-2 items-start text-secondary-foreground text-sm cursor-pointer select-none [&>button]:mt-0.5",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -31,14 +31,17 @@ DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;
|
||||
|
||||
const DialogContent = React.forwardRef<
|
||||
React.ElementRef<typeof DialogPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
|
||||
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
|
||||
dialogOverlayClassName?: string;
|
||||
dialogCloseClassName?: string;
|
||||
}
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay />
|
||||
<DialogOverlay className={props.dialogOverlayClassName} />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed grid w-full max-w-lg gap-4 border bg-background p-6 shadow-lg duration-300 rounded-lg border-border",
|
||||
"fixed grid w-full max-w-lg gap-4 border bg-background p-6 shadow-lg duration-300 rounded-t-xl md:rounded-lg border-border",
|
||||
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
|
||||
"bottom-0 left-0 right-0",
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
@@ -54,7 +57,12 @@ const DialogContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
||||
props.dialogCloseClassName,
|
||||
)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
"use client";
|
||||
|
||||
import * as React from "react";
|
||||
import { Drawer as DrawerPrimitive } from "vaul";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Drawer = ({
|
||||
shouldScaleBackground = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DrawerPrimitive.Root>) => (
|
||||
<DrawerPrimitive.Root
|
||||
shouldScaleBackground={shouldScaleBackground}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
Drawer.displayName = "Drawer";
|
||||
|
||||
const DrawerTrigger = DrawerPrimitive.Trigger;
|
||||
|
||||
const DrawerPortal = DrawerPrimitive.Portal;
|
||||
|
||||
const DrawerClose = DrawerPrimitive.Close;
|
||||
|
||||
const DrawerOverlay = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Overlay>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Overlay>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn("fixed inset-0 z-50 bg-black/80", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName;
|
||||
|
||||
const DrawerContent = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DrawerPortal>
|
||||
<DrawerOverlay />
|
||||
<DrawerPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-x-0 bottom-0 z-50 mt-24 flex h-auto flex-col rounded-t-[10px] border bg-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted" />
|
||||
{children}
|
||||
</DrawerPrimitive.Content>
|
||||
</DrawerPortal>
|
||||
));
|
||||
DrawerContent.displayName = "DrawerContent";
|
||||
|
||||
const DrawerHeader = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("grid gap-1.5 p-4 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DrawerHeader.displayName = "DrawerHeader";
|
||||
|
||||
const DrawerFooter = ({
|
||||
className,
|
||||
...props
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn("mt-auto flex flex-col gap-2 p-4", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
DrawerFooter.displayName = "DrawerFooter";
|
||||
|
||||
const DrawerTitle = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Title>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Title>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerTitle.displayName = DrawerPrimitive.Title.displayName;
|
||||
|
||||
const DrawerDescription = React.forwardRef<
|
||||
React.ElementRef<typeof DrawerPrimitive.Description>,
|
||||
React.ComponentPropsWithoutRef<typeof DrawerPrimitive.Description>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DrawerPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
DrawerDescription.displayName = DrawerPrimitive.Description.displayName;
|
||||
|
||||
export {
|
||||
Drawer,
|
||||
DrawerPortal,
|
||||
DrawerOverlay,
|
||||
DrawerTrigger,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerHeader,
|
||||
DrawerFooter,
|
||||
DrawerTitle,
|
||||
DrawerDescription,
|
||||
};
|
||||
@@ -75,7 +75,7 @@ const SelectContent = React.forwardRef<
|
||||
<SelectPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
|
||||
import { Button } from "./button";
|
||||
|
||||
|
||||
@@ -31,15 +31,24 @@ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
export function ToolTipLabel(props: {
|
||||
children: React.ReactNode;
|
||||
label: string;
|
||||
label: string | undefined;
|
||||
rightIcon?: React.ReactNode;
|
||||
leftIcon?: React.ReactNode;
|
||||
}) {
|
||||
if (!props.label) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100} disableHoverableContent={true}>
|
||||
<TooltipTrigger asChild>{props.children}</TooltipTrigger>
|
||||
<TooltipContent sideOffset={10}>
|
||||
<TooltipTrigger asChild className="!pointer-events-auto">
|
||||
{props.children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
sideOffset={10}
|
||||
className="max-w-[300px] leading-relaxed"
|
||||
>
|
||||
<div className="p-2 text-sm flex items-center gap-1.5">
|
||||
{props.leftIcon}
|
||||
{props.label}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const LOGGED_IN_ONLY_PATHS = [
|
||||
const LOGGED_IN_ONLY_PATHS = [
|
||||
// anything that _starts_ with /dashboard is logged in only
|
||||
"/dashboard",
|
||||
// anything that _starts_ with /cli is logged in only
|
||||
@@ -6,3 +6,12 @@ export const LOGGED_IN_ONLY_PATHS = [
|
||||
"/support",
|
||||
// TODO: add any other logged in only paths here
|
||||
];
|
||||
|
||||
export function isLoginRequired(pathname: string) {
|
||||
// exceptions from LOGGED_IN_ONLY_PATHS
|
||||
if (pathname.startsWith("/dashboard/connect/playground")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return LOGGED_IN_ONLY_PATHS.some((path) => pathname.startsWith(path));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { toast } from "sonner";
|
||||
|
||||
// if browser supports sharing use native sharing - use that, else just copy to clipboard + show toast
|
||||
export async function shareLink(_data: ShareData) {
|
||||
const data = { ..._data, url: _data.url || window.location.href };
|
||||
|
||||
if (
|
||||
"canShare" in navigator &&
|
||||
navigator.canShare &&
|
||||
navigator.canShare(data)
|
||||
) {
|
||||
await navigator.share(data);
|
||||
} else {
|
||||
await navigator.clipboard.writeText(data.url);
|
||||
toast.info("URL copied to clipboard");
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
--inverted: var(--foreground);
|
||||
|
||||
/* bg - colorful */
|
||||
--primary: 215.88 100% 55%;
|
||||
--primary: 221 83% 54%;
|
||||
--destructive: 0 62.8% 15%;
|
||||
--success: 156.2 71.6% 10%;
|
||||
--warning: 40.61deg 96.12% 8%;
|
||||
@@ -91,6 +91,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* shadcn charts */
|
||||
@layer base {
|
||||
:root {
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes skeleton {
|
||||
0%,
|
||||
100% {
|
||||
|
||||
@@ -14,13 +14,22 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { AutoConnect, ConnectButton } from "thirdweb/react";
|
||||
import type { Chain } from "thirdweb";
|
||||
import { AutoConnect, ConnectButton, useConnectModal } from "thirdweb/react";
|
||||
import { getSDKTheme } from "../../../../app/components/sdk-component-theme";
|
||||
import { CustomChainRenderer } from "../../../../components/selects/CustomChainRenderer";
|
||||
import { mapV4ChainToV5Chain } from "../../../../contexts/map-chains";
|
||||
import { useSetIsNetworkConfigModalOpen } from "../../../../hooks/networkConfigModal";
|
||||
import { useFavoriteChains } from "../../hooks/useFavoriteChains";
|
||||
import { useLoggedInUser } from "../../hooks/useLoggedInUser";
|
||||
import { popularChains } from "../popularChains";
|
||||
|
||||
export const CustomConnectWallet: React.FC = () => {
|
||||
export const CustomConnectWallet = (props: {
|
||||
loginRequired?: boolean;
|
||||
connectButtonClassName?: string;
|
||||
}) => {
|
||||
const loginRequired =
|
||||
props.loginRequired === undefined ? true : props.loginRequired;
|
||||
const { theme } = useTheme();
|
||||
const recentChainsv4 = useRecentlyUsedChains();
|
||||
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
|
||||
@@ -28,16 +37,16 @@ export const CustomConnectWallet: React.FC = () => {
|
||||
const t = theme === "light" ? "light" : "dark";
|
||||
const allv4Chains = useSupportedChains();
|
||||
const favChainsQuery = useFavoriteChains();
|
||||
|
||||
const setIsNetworkConfigModalOpen = useSetIsNetworkConfigModalOpen();
|
||||
const allChains = useMemo(() => {
|
||||
return allv4Chains.map((c) => defineChain(c));
|
||||
return allv4Chains.map(mapV4ChainToV5Chain);
|
||||
}, [allv4Chains]);
|
||||
|
||||
const chainSections = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: "Favorites",
|
||||
chains: favChainsQuery.data.map(defineChain),
|
||||
chains: favChainsQuery.data.map(mapV4ChainToV5Chain),
|
||||
},
|
||||
{
|
||||
label: "Popular",
|
||||
@@ -45,7 +54,7 @@ export const CustomConnectWallet: React.FC = () => {
|
||||
},
|
||||
{
|
||||
label: "Recent",
|
||||
chains: recentChainsv4.map(defineChain),
|
||||
chains: recentChainsv4.map(mapV4ChainToV5Chain),
|
||||
},
|
||||
];
|
||||
}, [recentChainsv4, favChainsQuery.data]);
|
||||
@@ -57,7 +66,7 @@ export const CustomConnectWallet: React.FC = () => {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-[144px] h-[48px] bg-muted border rounded-lg flex items-center justify-center">
|
||||
<div className="w-[144px] h-[48px] bg-muted border border-border rounded-lg flex items-center justify-center">
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{/* need autoconnect here so that we actually connect */}
|
||||
@@ -66,7 +75,7 @@ export const CustomConnectWallet: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoggedIn) {
|
||||
if (!isLoggedIn && loginRequired) {
|
||||
return (
|
||||
<>
|
||||
<Button asChild variant="default" className="gap-2" size="lg">
|
||||
@@ -84,7 +93,7 @@ export const CustomConnectWallet: React.FC = () => {
|
||||
|
||||
return (
|
||||
<ConnectButton
|
||||
theme={t}
|
||||
theme={getSDKTheme(t)}
|
||||
client={thirdwebClient}
|
||||
connectModal={{
|
||||
privacyPolicyUrl: "/privacy",
|
||||
@@ -107,6 +116,9 @@ export const CustomConnectWallet: React.FC = () => {
|
||||
console.error("Failed to log out", err);
|
||||
}
|
||||
}}
|
||||
connectButton={{
|
||||
className: props.connectButtonClassName,
|
||||
}}
|
||||
chains={allChains}
|
||||
detailsModal={{
|
||||
networkSelector: {
|
||||
@@ -114,8 +126,10 @@ export const CustomConnectWallet: React.FC = () => {
|
||||
onSwitch(chain) {
|
||||
addRecentlyUsedChainId(chain.id);
|
||||
},
|
||||
// TODO: bring this back when it works reliably
|
||||
// renderChain: CustomChainRenderer,
|
||||
renderChain: CustomChainRenderer,
|
||||
onCustomClick: () => {
|
||||
setIsNetworkConfigModalOpen(true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
@@ -193,6 +207,35 @@ export function ConnectWalletWelcomeScreen(props: {
|
||||
);
|
||||
}
|
||||
|
||||
export function useCustomConnectModal() {
|
||||
const { connect } = useConnectModal();
|
||||
const { theme } = useTheme();
|
||||
|
||||
return useCallback(
|
||||
(options?: { chain?: Chain }) => {
|
||||
return connect({
|
||||
client: thirdwebClient,
|
||||
appMetadata: {
|
||||
name: "thirdweb",
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
url: "https://thirdweb.com",
|
||||
},
|
||||
chain: options?.chain,
|
||||
privacyPolicyUrl: "/privacy",
|
||||
termsOfServiceUrl: "/tos",
|
||||
showThirdwebBranding: false,
|
||||
welcomeScreen: () => (
|
||||
<ConnectWalletWelcomeScreen
|
||||
theme={theme === "light" ? "light" : "dark"}
|
||||
/>
|
||||
),
|
||||
theme: getSDKTheme(theme === "light" ? "light" : "dark"),
|
||||
});
|
||||
},
|
||||
[connect, theme],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A link component extends the `Link` component and adds tracking.
|
||||
*/
|
||||
|
||||
@@ -2,11 +2,11 @@ import {
|
||||
useIsAdmin,
|
||||
useIsAdminOrSelf,
|
||||
} from "@3rdweb-sdk/react/hooks/useContractRoles";
|
||||
import type { ValidContractInstance } from "@thirdweb-dev/sdk";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ComponentWithChildren } from "types/component-with-children";
|
||||
|
||||
interface AdminOnlyProps {
|
||||
contract?: ValidContractInstance;
|
||||
contract: ThirdwebContract;
|
||||
fallback?: JSX.Element;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,9 @@ export const AdminOnly: ComponentWithChildren<AdminOnlyProps> = ({
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
interface AdminOrSelfOnlyProps extends AdminOnlyProps {
|
||||
interface AdminOrSelfOnlyProps {
|
||||
contract: ThirdwebContract;
|
||||
fallback?: JSX.Element;
|
||||
/**
|
||||
* The address of the account to check against
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useIsLister } from "@3rdweb-sdk/react/hooks/useContractRoles";
|
||||
import type { ValidContractInstance } from "@thirdweb-dev/sdk";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ComponentWithChildren } from "types/component-with-children";
|
||||
|
||||
interface IListerOnlyProps {
|
||||
contract?: ValidContractInstance;
|
||||
contract: ThirdwebContract;
|
||||
}
|
||||
|
||||
export const ListerOnly: ComponentWithChildren<IListerOnlyProps> = ({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useIsMinter } from "@3rdweb-sdk/react/hooks/useContractRoles";
|
||||
import type { ValidContractInstance } from "@thirdweb-dev/sdk";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ComponentWithChildren } from "types/component-with-children";
|
||||
|
||||
interface IMinterOnlyProps {
|
||||
contract?: ValidContractInstance;
|
||||
contract: ThirdwebContract;
|
||||
}
|
||||
|
||||
export const MinterOnly: ComponentWithChildren<IMinterOnlyProps> = ({
|
||||
|
||||
@@ -4,9 +4,9 @@ import {
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import invariant from "tiny-invariant";
|
||||
import { THIRDWEB_API_HOST } from "../../../constants/urls";
|
||||
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
|
||||
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
|
||||
import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
@@ -1,110 +1,73 @@
|
||||
import {
|
||||
type ContractWithRoles,
|
||||
type RolesForContract,
|
||||
useContractType,
|
||||
useRoleMembers,
|
||||
} from "@thirdweb-dev/react";
|
||||
import type { ValidContractInstance } from "@thirdweb-dev/sdk";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
|
||||
import { hasRole } from "thirdweb/extensions/permissions";
|
||||
import { useActiveAccount, useReadContract } from "thirdweb/react";
|
||||
import type { RequiredParam } from "utils/types";
|
||||
|
||||
function isContractWithRoles(
|
||||
contract: RequiredParam<ValidContractInstance>,
|
||||
): contract is ContractWithRoles {
|
||||
if (contract && "roles" in contract) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function useIsAccountRole<TContract extends ContractWithRoles>(
|
||||
role: RolesForContract<TContract>[number],
|
||||
contract: RequiredParam<TContract>,
|
||||
account: RequiredParam<string>,
|
||||
): boolean {
|
||||
const contractHasRoles = isContractWithRoles(contract);
|
||||
const { data } = useRoleMembers(contract, role);
|
||||
|
||||
if (contractHasRoles === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (data?.includes(ZERO_ADDRESS)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!(account && data?.includes(account));
|
||||
}
|
||||
|
||||
export function useIsAdmin<TContract extends ValidContractInstance>(
|
||||
contract: RequiredParam<TContract>,
|
||||
) {
|
||||
export function useIsAdmin(contract: ThirdwebContract) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const { data: contractType } = useContractType(contract?.getAddress());
|
||||
const { data: userIsAdmin, isError } = useReadContract(hasRole, {
|
||||
contract,
|
||||
targetAccountAddress: address ?? "",
|
||||
role: "admin",
|
||||
queryOptions: { enabled: !!address },
|
||||
});
|
||||
|
||||
const contractHasRoles = isContractWithRoles(contract);
|
||||
const isAccountRole = useIsAccountRole(
|
||||
"admin",
|
||||
contractHasRoles ? contract : undefined,
|
||||
address,
|
||||
);
|
||||
|
||||
if (contractType === "custom") {
|
||||
return true;
|
||||
}
|
||||
return isAccountRole;
|
||||
// If the request results in an error, it's likely that the contract is a non-thirdweb contract
|
||||
// in that case we return `true` anyway
|
||||
return userIsAdmin || isError;
|
||||
}
|
||||
|
||||
export function useIsAdminOrSelf<TContract extends ValidContractInstance>(
|
||||
contract: RequiredParam<TContract>,
|
||||
export function useIsAdminOrSelf(
|
||||
contract: ThirdwebContract,
|
||||
self: RequiredParam<string>,
|
||||
) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const { data: contractType } = useContractType(contract?.getAddress());
|
||||
const isAdmin = useIsAdmin(contract);
|
||||
|
||||
if (address === self) {
|
||||
return true;
|
||||
}
|
||||
if (contractType === "custom") {
|
||||
return true;
|
||||
}
|
||||
return isAdmin;
|
||||
}
|
||||
|
||||
export function useIsMinter<TContract extends ValidContractInstance>(
|
||||
contract: RequiredParam<TContract>,
|
||||
) {
|
||||
export function useIsMinter(contract: ThirdwebContract) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const { data: contractType } = useContractType(contract?.getAddress());
|
||||
const contractHasRoles = isContractWithRoles(contract);
|
||||
const isAccountRole = useIsAccountRole(
|
||||
"minter",
|
||||
contractHasRoles ? contract : undefined,
|
||||
address,
|
||||
);
|
||||
const userCanMintQuery = useReadContract(hasRole, {
|
||||
contract,
|
||||
targetAccountAddress: address ?? "",
|
||||
role: "minter",
|
||||
queryOptions: { enabled: !!address },
|
||||
});
|
||||
|
||||
if (contractType === "custom") {
|
||||
return true;
|
||||
}
|
||||
return isAccountRole;
|
||||
const anyoneCanMintQuery = useReadContract(hasRole, {
|
||||
contract,
|
||||
targetAccountAddress: ZERO_ADDRESS,
|
||||
role: "minter",
|
||||
});
|
||||
|
||||
// If both requests "error out", we can safely assume that
|
||||
// this is a custom (aka. non-thirdweb) contract
|
||||
// so we just return `true` in that case
|
||||
const isCustomContract =
|
||||
userCanMintQuery.isError && anyoneCanMintQuery.isError;
|
||||
|
||||
return anyoneCanMintQuery.data || userCanMintQuery.data || isCustomContract;
|
||||
}
|
||||
|
||||
export function useIsLister<TContract extends ValidContractInstance>(
|
||||
contract: RequiredParam<TContract>,
|
||||
) {
|
||||
// Applicable to Marketplace v3 only
|
||||
export function useIsLister(contract: ThirdwebContract) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const { data: contractType } = useContractType(contract?.getAddress());
|
||||
const contractHasRoles = isContractWithRoles(contract);
|
||||
const isAccountRole = useIsAccountRole(
|
||||
"lister",
|
||||
contractHasRoles ? contract : undefined,
|
||||
address,
|
||||
);
|
||||
const { data: userCanList } = useReadContract(hasRole, {
|
||||
contract,
|
||||
targetAccountAddress: address ?? "",
|
||||
role: "lister",
|
||||
queryOptions: { enabled: !!address },
|
||||
});
|
||||
|
||||
if (contractType === "custom") {
|
||||
return true;
|
||||
}
|
||||
return isAccountRole;
|
||||
const { data: anyoneCanList } = useReadContract(hasRole, {
|
||||
contract,
|
||||
targetAccountAddress: ZERO_ADDRESS,
|
||||
role: "lister",
|
||||
});
|
||||
|
||||
return anyoneCanList || userCanList;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { upload } from "thirdweb/storage";
|
||||
|
||||
type DashboardUploadOptions = {
|
||||
uploadWithoutDirectory?: boolean;
|
||||
metadata?: Record<string, string>;
|
||||
};
|
||||
|
||||
export function useDashboardStorageUpload(options?: DashboardUploadOptions) {
|
||||
return useMutation({
|
||||
mutationFn: async (files: Array<File | string>): Promise<string[]> => {
|
||||
const uris = await upload({
|
||||
client: thirdwebClient,
|
||||
files,
|
||||
...options,
|
||||
});
|
||||
// the upload method in SDK v5 has this logic
|
||||
// that will make it return only a string if you upload only 1 file
|
||||
if (typeof uris === "string") {
|
||||
return [uris];
|
||||
}
|
||||
return uris;
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { ResultItem } from "components/engine/system-metrics/components/StatusCodes";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
@@ -121,7 +122,10 @@ export interface EngineSystemHealth {
|
||||
features?: string[];
|
||||
}
|
||||
|
||||
export function useEngineSystemHealth(instanceUrl: string) {
|
||||
export function useEngineSystemHealth(
|
||||
instanceUrl: string,
|
||||
pollInterval: number | false = false,
|
||||
) {
|
||||
return useQuery(
|
||||
engineKeys.health(instanceUrl),
|
||||
async () => {
|
||||
@@ -134,7 +138,10 @@ export function useEngineSystemHealth(instanceUrl: string) {
|
||||
const json = (await res.json()) as EngineSystemHealth;
|
||||
return json;
|
||||
},
|
||||
{ enabled: !!instanceUrl },
|
||||
{
|
||||
enabled: !!instanceUrl,
|
||||
refetchInterval: pollInterval,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1449,6 +1456,9 @@ export interface EngineResourceMetrics {
|
||||
data: {
|
||||
cpu: number;
|
||||
memory: number;
|
||||
errorRate: ResultItem[];
|
||||
statusCodes: ResultItem[];
|
||||
requestVolume: ResultItem[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1460,9 +1470,6 @@ export function useEngineResourceMetrics(engineId: string) {
|
||||
async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/metrics`,
|
||||
{
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
setEnabled(false);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useSupportedChains } from "@thirdweb-dev/react";
|
||||
import { useFavouriteChainIds } from "app/(dashboard)/(chain)/components/client/star-button";
|
||||
import { useMemo } from "react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useFavouriteChainIds } from "../../../app/(dashboard)/(chain)/components/client/star-button";
|
||||
|
||||
export function useFavoriteChains() {
|
||||
const allChains = useSupportedChains();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { LOGGED_IN_ONLY_PATHS } from "@/constants/auth";
|
||||
import { isLoginRequired } from "@/constants/auth";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { EnsureLoginResponse } from "app/api/auth/ensure-login/route";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { useRef } from "react";
|
||||
import {
|
||||
useActiveAccount,
|
||||
useActiveWalletConnectionStatus,
|
||||
} from "thirdweb/react";
|
||||
import type { EnsureLoginResponse } from "../../../app/api/auth/ensure-login/route";
|
||||
|
||||
// define "TW_AUTH_TOKEN" to exist on the window object
|
||||
declare global {
|
||||
@@ -35,12 +35,12 @@ export function useLoggedInUser(): {
|
||||
// enabled if:
|
||||
// - there is a pathname
|
||||
// - we are not already currently connecting
|
||||
// - the pathname is one of the LOGGED_IN_ONLY_PATHS
|
||||
// - the pathname requires login
|
||||
enabled:
|
||||
!!pathname &&
|
||||
connectionStatus !== "connecting" &&
|
||||
statusWasEverConnecting.current &&
|
||||
LOGGED_IN_ONLY_PATHS.some((path) => pathname.startsWith(path)),
|
||||
isLoginRequired(pathname),
|
||||
// the last "persist", part of the queryKey, is to make sure that we do not cache this query in indexDB
|
||||
// convention in v4 of the SDK that we are (ab)using here
|
||||
queryKey: ["logged_in_user", connectedAddress, { persist: false }],
|
||||
@@ -113,7 +113,7 @@ export function useLoggedInUser(): {
|
||||
}
|
||||
|
||||
// if we are not on a logged in path, we can simply return the connected address
|
||||
if (!LOGGED_IN_ONLY_PATHS.some((path) => pathname.startsWith(path))) {
|
||||
if (!isLoginRequired(pathname)) {
|
||||
return {
|
||||
isLoading: false,
|
||||
isLoggedIn: true,
|
||||
|
||||
@@ -8,8 +8,8 @@ import type { BasicContract } from "contract-ui/types/types";
|
||||
import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
|
||||
import { defineDashboardChain } from "lib/defineDashboardChain";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { defineDashboardChain } from "lib/v5-adapter";
|
||||
import { useMemo } from "react";
|
||||
import { getContract } from "thirdweb";
|
||||
import { polygon } from "thirdweb/chains";
|
||||
@@ -22,7 +22,8 @@ const MULTICHAIN_REGISTRY_ADDRESS =
|
||||
const registryContract = getContract({
|
||||
client: thirdwebClient,
|
||||
address: MULTICHAIN_REGISTRY_ADDRESS,
|
||||
chain: defineDashboardChain(polygon.id),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineDashboardChain(polygon.id, undefined),
|
||||
});
|
||||
|
||||
function useMultiChainRegContractList(walletAddress?: string) {
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getChain } from "../../../utils";
|
||||
import { InfoCard } from "../components/server/info-card";
|
||||
|
||||
export default async function Page(props: { params: { chain_id: string } }) {
|
||||
const chain = await getChain(props.params.chain_id);
|
||||
const enabled = chain.services.find(
|
||||
(s) => s.service === "account-abstraction",
|
||||
)?.enabled;
|
||||
|
||||
if (!enabled) {
|
||||
redirect(`/${props.params.chain_id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InfoCard
|
||||
title="Account Abstraction"
|
||||
links={[
|
||||
{
|
||||
label: "Get Started",
|
||||
href: "/dashboard/connect/account-abstraction",
|
||||
},
|
||||
{
|
||||
label: "Learn More",
|
||||
href: "https://portal.thirdweb.com/connect/account-abstraction/how-it-works",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>
|
||||
Everything you need to leverage account abstraction technology to
|
||||
enable seamless user experiences for your users.
|
||||
</p>
|
||||
<p>
|
||||
You get all the tools to integrate account abstraction into your app.
|
||||
This includes:
|
||||
</p>
|
||||
|
||||
<div className="h-3" />
|
||||
|
||||
<ul className="text-secondary-foreground pl-4 [&_li]:list-disc [&_li]:mb-3 [&_li]:pl-1">
|
||||
<li>
|
||||
Account factory contracts that let you spin up smart accounts for
|
||||
your users
|
||||
</li>
|
||||
|
||||
<li>
|
||||
Bundler, which is the infrastructure needed to process account
|
||||
abstraction transactions (known as User Ops)
|
||||
</li>
|
||||
|
||||
<li>
|
||||
Paymaster, which lets you sponsor transaction fees for your users
|
||||
</li>
|
||||
</ul>
|
||||
</InfoCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { toast } from "sonner";
|
||||
import { toUnits } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../../../contexts/map-chains";
|
||||
|
||||
function formatTime(seconds: number) {
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
if (hours > 0) {
|
||||
return rtf.format(+hours, "hour");
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
return rtf.format(+minutes, "minute");
|
||||
}
|
||||
|
||||
return rtf.format(+seconds, "second");
|
||||
}
|
||||
|
||||
export function FaucetButton({
|
||||
chain,
|
||||
amount,
|
||||
}: {
|
||||
chain: ChainMetadata;
|
||||
amount: string;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const chainId = chain.chainId;
|
||||
// do not include local overrides for chain pages
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const definedChain = mapV4ChainToV5Chain(chain);
|
||||
const faucetWalletBalanceQuery = useWalletBalance({
|
||||
address: THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
chain: definedChain,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
const trackEvent = useTrack();
|
||||
const claimMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
trackEvent({
|
||||
category: "faucet",
|
||||
action: "claim",
|
||||
label: "attempt",
|
||||
chain_id: chainId,
|
||||
});
|
||||
const response = await fetch("/api/testnet-faucet/claim", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chainId: chainId,
|
||||
toAddress: address,
|
||||
amount,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json();
|
||||
throw new Error(data?.error || "Failed to claim funds");
|
||||
}
|
||||
},
|
||||
onSuccess: () => {
|
||||
trackEvent({
|
||||
category: "faucet",
|
||||
action: "claim",
|
||||
label: "success",
|
||||
chain_id: chainId,
|
||||
});
|
||||
router.refresh();
|
||||
},
|
||||
onError: (error) => {
|
||||
trackEvent({
|
||||
category: "faucet",
|
||||
action: "claim",
|
||||
label: "error",
|
||||
chain_id: chainId,
|
||||
errorMsg: error instanceof Error ? error.message : "Unknown error",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const canClaimFaucetQuery = useQuery({
|
||||
queryKey: ["testnet-faucet-can-claim", chainId],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(
|
||||
`/api/testnet-faucet/can-claim?chainId=${chainId}`,
|
||||
);
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to get claim status");
|
||||
}
|
||||
const data = (await response.json()) as CanClaimResponseType;
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const isFaucetEmpty =
|
||||
faucetWalletBalanceQuery.data !== undefined &&
|
||||
faucetWalletBalanceQuery.data.value < toUnits("1", 17);
|
||||
|
||||
// loading state
|
||||
if (faucetWalletBalanceQuery.isLoading || canClaimFaucetQuery.isLoading) {
|
||||
return (
|
||||
<Button variant="secondary" className="w-full gap-2">
|
||||
Checking Faucet <Spinner className="size-3" />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// faucet is empty
|
||||
if (isFaucetEmpty) {
|
||||
return (
|
||||
<Button variant="secondary" disabled className="w-full !opacity-100 ">
|
||||
Faucet is empty right now
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
// Can not claim
|
||||
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
|
||||
return (
|
||||
<Button variant="secondary" className="w-full !opacity-100 " disabled>
|
||||
{canClaimFaucetQuery.data.type === "throttle" && (
|
||||
<>
|
||||
Your next claim is available{" "}
|
||||
{formatTime(canClaimFaucetQuery.data.ttlSeconds)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{canClaimFaucetQuery.data.type === "unsupported-chain" &&
|
||||
"Faucet is empty right now"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
if (!address) {
|
||||
return (
|
||||
<CustomConnectWallet
|
||||
loginRequired={false}
|
||||
connectButtonClassName="!w-full !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// eligible to claim and faucet has balance
|
||||
return (
|
||||
<div className="flex flex-col w-full text-center">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full gap-2"
|
||||
onClick={() => {
|
||||
const claimPromise = claimMutation.mutateAsync();
|
||||
toast.promise(claimPromise, {
|
||||
success: `${amount} ${chain.nativeCurrency.symbol} sent successfully`,
|
||||
error: `Failed to claim ${amount} ${chain.nativeCurrency.symbol}`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{claimMutation.isLoading ? (
|
||||
<>
|
||||
Claiming <Spinner className="size-3" />
|
||||
</>
|
||||
) : (
|
||||
`Get ${amount} ${chain.nativeCurrency.symbol}`
|
||||
)}
|
||||
</Button>
|
||||
|
||||
{faucetWalletBalanceQuery.data && (
|
||||
<p className="mt-3 text-xs text-secondary-foreground">
|
||||
{Number(faucetWalletBalanceQuery.data.displayValue).toFixed(3)}{" "}
|
||||
{faucetWalletBalanceQuery.data.symbol} left in the faucet
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { defineDashboardChain } from "lib/defineDashboardChain";
|
||||
import { useTheme } from "next-themes";
|
||||
import { PayEmbed } from "thirdweb/react";
|
||||
import { getSDKTheme } from "../../../../../../components/sdk-component-theme";
|
||||
|
||||
export function PayModalButton(props: { chainId: number; label: string }) {
|
||||
const { theme } = useTheme();
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="primary" className="w-full">
|
||||
{props.label}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="md:max-w-[360px] p-0 border-none bg-transparent"
|
||||
dialogOverlayClassName="backdrop-blur-lg"
|
||||
dialogCloseClassName="focus:ring-0"
|
||||
>
|
||||
<PayEmbed
|
||||
client={thirdwebClient}
|
||||
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
|
||||
className="!w-auto"
|
||||
payOptions={{
|
||||
prefillBuy: {
|
||||
// Do not include local chain overrides for chain pages
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineDashboardChain(props.chainId, undefined),
|
||||
allowEdits: {
|
||||
chain: false,
|
||||
amount: true,
|
||||
token: true,
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,9 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCustomConnectModal } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import type { Chain } from "thirdweb";
|
||||
import {
|
||||
useActiveAccount,
|
||||
@@ -14,56 +15,54 @@ import {
|
||||
import { useDebounce } from "use-debounce";
|
||||
|
||||
type AddChainToWalletProps = {
|
||||
chain?: Chain;
|
||||
hasBackground?: boolean;
|
||||
chain: Chain;
|
||||
};
|
||||
|
||||
export const AddChainToWallet: React.FC<AddChainToWalletProps> = (props) => {
|
||||
const address = useActiveAccount()?.address;
|
||||
const account = useActiveAccount();
|
||||
const switchChain = useSwitchActiveWalletChain();
|
||||
const activeWalletChainId = useActiveWalletChain()?.id;
|
||||
const customConnectModal = useCustomConnectModal();
|
||||
|
||||
const switchChainMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
if (!props.chain) {
|
||||
throw new Error("Chain is not defined");
|
||||
}
|
||||
await switchChain(props.chain);
|
||||
},
|
||||
});
|
||||
|
||||
// debounce the loading state to prevent flickering
|
||||
const [debouncedLoading] = useDebounce(switchChainMutation.isLoading, 50);
|
||||
|
||||
const buttonContent = (
|
||||
<Button
|
||||
disabled={
|
||||
!address || debouncedLoading || activeWalletChainId === props.chain?.id
|
||||
}
|
||||
className={cn(
|
||||
"w-full md:min-w-40",
|
||||
props.hasBackground && "invert dark:invert-0",
|
||||
)}
|
||||
onClick={() => switchChainMutation.mutate()}
|
||||
>
|
||||
{debouncedLoading && <Spinner />}
|
||||
<span>Add to wallet</span>
|
||||
</Button>
|
||||
);
|
||||
|
||||
if (address && activeWalletChainId !== props.chain?.id) {
|
||||
return buttonContent;
|
||||
}
|
||||
const disabled = debouncedLoading || activeWalletChainId === props.chain?.id;
|
||||
|
||||
return (
|
||||
<ToolTipLabel
|
||||
label={
|
||||
activeWalletChainId === props.chain?.id
|
||||
? "You are already connected to this chain"
|
||||
: "Connect your wallet first"
|
||||
activeWalletChainId === props.chain.id
|
||||
? "Wallet is already connected to this chain"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<div className="cursor-not-allowed">{buttonContent}</div>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
className="gap-2 w-full"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Connct directly to this chain
|
||||
if (!account) {
|
||||
return customConnectModal({ chain: props.chain });
|
||||
}
|
||||
|
||||
// switch to this chain
|
||||
const switchPromise = switchChainMutation.mutateAsync();
|
||||
toast.promise(switchPromise, {
|
||||
success: `${props.chain.name} added to wallet`,
|
||||
error: `Failed to add ${props.chain.name} to wallet`,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span>Add to wallet</span>
|
||||
{debouncedLoading && <Spinner className="size-3" />}
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CopyButton } from "@/components/ui/CopyButton";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -9,7 +9,7 @@ import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { CircleCheck, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { hostnameEndsWith } from "utils/url";
|
||||
import { PrimaryInfoItem } from "../../../components/server/primary-info-item";
|
||||
import { PrimaryInfoItem } from "../server/primary-info-item";
|
||||
|
||||
function useChainStatswithRPC(_rpcUrl: string) {
|
||||
const [shouldRefetch, setShouldRefetch] = useState(true);
|
||||
@@ -79,19 +79,24 @@ export function ChainLiveStats(props: { rpc: string }) {
|
||||
}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<div className="text-lg">{new URL(props.rpc).origin}</div>
|
||||
<CopyButton text={new URL(props.rpc).origin} />
|
||||
<CopyTextButton
|
||||
tooltip="Copy RPC URL"
|
||||
textToShow={new URL(props.rpc).origin}
|
||||
textToCopy={props.rpc}
|
||||
copyIconPosition="right"
|
||||
variant="ghost"
|
||||
className="px-2 py-1 -translate-x-2 text-base"
|
||||
iconClassName="size-4"
|
||||
/>
|
||||
</div>
|
||||
</PrimaryInfoItem>
|
||||
|
||||
{/* Latency */}
|
||||
<PrimaryInfoItem title="RPC Latency" titleIcon={<PulseDot />}>
|
||||
{stats.isError ? (
|
||||
<p className="text-lg fade-in-0 animate-in text-destructive-text">
|
||||
N/A
|
||||
</p>
|
||||
<p className="fade-in-0 animate-in text-destructive-text">N/A</p>
|
||||
) : stats.data ? (
|
||||
<p className="text-lg fade-in-0 animate-in">{stats.data.latency}ms</p>
|
||||
<p className="fade-in-0 animate-in">{stats.data.latency}ms</p>
|
||||
) : (
|
||||
<div className="flex py-1 h-[28px] w-[70px]">
|
||||
<Skeleton className="h-full w-full" />
|
||||
@@ -102,13 +107,9 @@ export function ChainLiveStats(props: { rpc: string }) {
|
||||
{/* Block Height */}
|
||||
<PrimaryInfoItem title="Block Height" titleIcon={<PulseDot />}>
|
||||
{stats.isError ? (
|
||||
<p className="text-lg fade-in-0 animate-in text-destructive-text">
|
||||
N/A
|
||||
</p>
|
||||
<p className="fade-in-0 animate-in text-destructive-text">N/A</p>
|
||||
) : stats.data ? (
|
||||
<p className="text-lg fade-in-0 animate-in">
|
||||
{stats.data.blockNumber}
|
||||
</p>
|
||||
<p className="fade-in-0 animate-in">{stats.data.blockNumber}</p>
|
||||
) : (
|
||||
<div className="flex py-1 h-[28px] w-[140px]">
|
||||
<Skeleton className="h-full w-full" />
|
||||
@@ -122,9 +123,9 @@ export function ChainLiveStats(props: { rpc: string }) {
|
||||
function PulseDot() {
|
||||
return (
|
||||
<ToolTipLabel label={"Live Data"}>
|
||||
<span className="relative flex size-3">
|
||||
<span className="relative flex size-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full size-3 bg-primary" />
|
||||
<span className="relative inline-flex rounded-full size-2 bg-primary" />
|
||||
</span>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
@@ -1,50 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { TabLinks } from "@/components/ui/tabs";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
import { useMemo } from "react";
|
||||
import { products } from "../../../../components/server/products";
|
||||
import type { ChainSupportedService } from "../../../../types/chain";
|
||||
|
||||
export function getEnabledTabs(enabledServices: ChainSupportedService[]) {
|
||||
return products
|
||||
.map((p) => ({
|
||||
name: p.name,
|
||||
segment: p.id === "contracts" ? "" : p.id,
|
||||
serviceId: p.id,
|
||||
icon: p.icon,
|
||||
isEnabled: enabledServices.includes(p.id) ?? false,
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (a.isEnabled && !b.isEnabled) {
|
||||
return -1;
|
||||
}
|
||||
if (!a.isEnabled && b.isEnabled) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
export function ChainPageTabs(props: {
|
||||
chainSlug: string;
|
||||
enabledServices: ChainSupportedService[];
|
||||
}) {
|
||||
const layoutSegment = useSelectedLayoutSegment() || "";
|
||||
|
||||
const links = useMemo(() => {
|
||||
return getEnabledTabs(props.enabledServices);
|
||||
}, [props.enabledServices]);
|
||||
|
||||
return (
|
||||
<TabLinks
|
||||
links={links.map((tab) => ({
|
||||
name: tab.name,
|
||||
href: `/${props.chainSlug}/${tab.segment}`,
|
||||
isActive: layoutSegment === tab.segment,
|
||||
isEnabled: tab.isEnabled,
|
||||
icon: tab.icon,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
export function CreditCardIcon(props: {
|
||||
bg: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width={48}
|
||||
height={48}
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
className={props.className}
|
||||
>
|
||||
<rect width={48} height={48} rx={16} fill={props.bg} />
|
||||
<rect
|
||||
x={1}
|
||||
y={1}
|
||||
width={46}
|
||||
height={46}
|
||||
rx={15}
|
||||
stroke="url(#paint0_linear_1315_3638)"
|
||||
strokeOpacity="0.2"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<rect x={5} y={5} width={38} height={38} rx={11} fill={props.bg} />
|
||||
<rect
|
||||
x={5}
|
||||
y={5}
|
||||
width={38}
|
||||
height={38}
|
||||
rx={11}
|
||||
stroke="url(#paint1_linear_1315_3638)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<path
|
||||
d="M32.3337 22.3333H15.667M23.167 25.6666H19.0003M15.667 20.8333L15.667 27.1666C15.667 28.1 15.667 28.5668 15.8486 28.9233C16.0084 29.2369 16.2634 29.4918 16.577 29.6516C16.9335 29.8333 17.4002 29.8333 18.3337 29.8333L29.667 29.8333C30.6004 29.8333 31.0671 29.8333 31.4236 29.6516C31.7372 29.4918 31.9922 29.2369 32.152 28.9233C32.3337 28.5668 32.3337 28.1 32.3337 27.1666V20.8333C32.3337 19.8999 32.3337 19.4332 32.152 19.0766C31.9922 18.763 31.7372 18.5081 31.4236 18.3483C31.0671 18.1666 30.6004 18.1666 29.667 18.1666L18.3337 18.1666C17.4002 18.1666 16.9335 18.1666 16.577 18.3483C16.2634 18.5081 16.0084 18.763 15.8486 19.0766C15.667 19.4332 15.667 19.8999 15.667 20.8333Z"
|
||||
stroke="url(#paint2_linear_1315_3638)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1315_3638"
|
||||
x1="-3.26045"
|
||||
y1="15.4021"
|
||||
x2="45.7807"
|
||||
y2="50.1547"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset={1} stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1315_3638"
|
||||
x1="1.28296"
|
||||
y1="16.835"
|
||||
x2="42.1506"
|
||||
y2="45.7956"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset={1} stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_1315_3638"
|
||||
x1="14.5349"
|
||||
y1="21.9102"
|
||||
x2="27.1676"
|
||||
y2="34.6989"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset={1} stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
export function GiftIcon(props: {
|
||||
bg: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
width={48}
|
||||
height={48}
|
||||
viewBox="0 0 48 48"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect width={48} height={48} rx={16} fill={props.bg} />
|
||||
<rect
|
||||
x={1}
|
||||
y={1}
|
||||
width={46}
|
||||
height={46}
|
||||
rx={15}
|
||||
stroke="url(#paint0_linear_1214_43510)"
|
||||
strokeOpacity="0.2"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<rect x={5} y={5} width={38} height={38} rx={11} fill={props.bg} />
|
||||
<rect
|
||||
x={5}
|
||||
y={5}
|
||||
width={38}
|
||||
height={38}
|
||||
rx={11}
|
||||
stroke="url(#paint1_linear_1214_43510)"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<g clipPath="url(#clip0_1214_43510)">
|
||||
<path
|
||||
d="M23.9993 19.0001V32.3334M23.9993 19.0001H21.0529C20.6188 19.0001 20.2024 18.8245 19.8955 18.5119C19.5885 18.1994 19.416 17.7754 19.416 17.3334C19.416 16.8914 19.5885 16.4675 19.8955 16.1549C20.2024 15.8423 20.6188 15.6667 21.0529 15.6667C23.3446 15.6667 23.9993 19.0001 23.9993 19.0001ZM23.9993 19.0001H26.9458C27.3799 19.0001 27.7963 18.8245 28.1032 18.5119C28.4102 18.1994 28.5827 17.7754 28.5827 17.3334C28.5827 16.8914 28.4102 16.4675 28.1032 16.1549C27.7963 15.8423 27.3799 15.6667 26.9458 15.6667C24.6541 15.6667 23.9993 19.0001 23.9993 19.0001ZM30.666 23.1667V29.6667C30.666 30.6002 30.666 31.0669 30.4844 31.4234C30.3246 31.737 30.0696 31.992 29.756 32.1518C29.3995 32.3334 28.9328 32.3334 27.9993 32.3334L19.9993 32.3334C19.0659 32.3334 18.5992 32.3334 18.2427 32.1518C17.9291 31.992 17.6741 31.737 17.5143 31.4234C17.3327 31.0669 17.3327 30.6002 17.3327 29.6667V23.1667M15.666 20.3334L15.666 21.8334C15.666 22.3001 15.666 22.5335 15.7568 22.7117C15.8367 22.8685 15.9642 22.996 16.121 23.0759C16.2993 23.1667 16.5326 23.1667 16.9993 23.1667L30.9994 23.1667C31.4661 23.1667 31.6994 23.1667 31.8777 23.0759C32.0345 22.996 32.162 22.8685 32.2419 22.7117C32.3327 22.5335 32.3327 22.3001 32.3327 21.8334V20.3334C32.3327 19.8667 32.3327 19.6333 32.2419 19.4551C32.162 19.2983 32.0345 19.1708 31.8777 19.0909C31.6994 19.0001 31.4661 19.0001 30.9993 19.0001L16.9993 19.0001C16.5326 19.0001 16.2993 19.0001 16.121 19.0909C15.9642 19.1708 15.8367 19.2983 15.7568 19.4551C15.666 19.6333 15.666 19.8667 15.666 20.3334Z"
|
||||
stroke="url(#paint2_linear_1214_43510)"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1214_43510"
|
||||
x1="-3.26045"
|
||||
y1="15.4021"
|
||||
x2="45.7807"
|
||||
y2="50.1547"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset={1} stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_1214_43510"
|
||||
x1="1.28296"
|
||||
y1="16.835"
|
||||
x2="42.1506"
|
||||
y2="45.7956"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset={1} stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_1214_43510"
|
||||
x1="14.5339"
|
||||
y1="21.0147"
|
||||
x2="31.5621"
|
||||
y2="33.0816"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#F213A4" />
|
||||
<stop offset="0.1517" stopColor="#E011A7" />
|
||||
<stop offset="0.4554" stopColor="#B20DAF" />
|
||||
<stop offset="0.8789" stopColor="#6806BB" />
|
||||
<stop offset={1} stopColor="#5204BF" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_1214_43510">
|
||||
<rect
|
||||
width={20}
|
||||
height={20}
|
||||
fill="white"
|
||||
transform="translate(14 14)"
|
||||
/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
import { PayModalButton } from "../client/PayModal";
|
||||
import { CreditCardIcon } from "../icons/CreditCardIcon";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
const sanitizedChainName = props.chain.name.replace("Mainnet", "").trim();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Buy Funds" />
|
||||
<div className="border border-border px-4 py-10 rounded-lg flex justify-center">
|
||||
<div className="max-w-[520px] flex flex-col items-center ">
|
||||
<div className="flex items-center">
|
||||
<ChainIcon
|
||||
className="size-12 -mr-2 rounded-full border p-1"
|
||||
iconUrl={props.chain.icon?.url}
|
||||
/>
|
||||
<CreditCardIcon
|
||||
bg="hsl(var(--background))"
|
||||
className="size-12 -ml-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<h2 className="text-lg tracking-tight font-semibold text-center px-4">
|
||||
Buy Funds on {sanitizedChainName} using thirdweb Pay
|
||||
</h2>
|
||||
|
||||
<div className="h-2" />
|
||||
|
||||
<p className="text-secondary-foreground text-sm max-w-[520px] text-center px-4">
|
||||
Instantly onramp, bridge & swap into {sanitizedChainName} with any
|
||||
token.
|
||||
</p>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<PayModalButton
|
||||
chainId={props.chain.chainId}
|
||||
label={`Buy Funds on ${sanitizedChainName}`}
|
||||
/>
|
||||
|
||||
<div className="h-4" />
|
||||
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/connect/pay/overview"
|
||||
className="inline-flex gap-1.5 items-center text-sm text-secondary-foreground hover:text-foreground"
|
||||
>
|
||||
Learn more about thirdweb Pay{" "}
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ChainLiveStats } from "../client/live-stats";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
import { PrimaryInfoItem } from "./primary-info-item";
|
||||
|
||||
export function ChainOverviewSection(props: { chain: ChainMetadata }) {
|
||||
const { chain } = props;
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Chain Overview" />
|
||||
<div className="grid grid-cols-1 gap-6 lg:gap-8 md:grid-cols-2 lg:grid-cols-3 border rounded-lg p-4 md:p-6">
|
||||
{/* Info */}
|
||||
{chain.infoURL && (
|
||||
<PrimaryInfoItem title="Info">
|
||||
<Link
|
||||
href={chain.infoURL}
|
||||
target="_blank"
|
||||
className="inline-flex items-center gap-1.5 hover:text-link-foreground"
|
||||
>
|
||||
{new URL(chain.infoURL).hostname}
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</Link>
|
||||
</PrimaryInfoItem>
|
||||
)}
|
||||
|
||||
{/* Chain Id */}
|
||||
<PrimaryInfoItem title="Chain ID">
|
||||
<CopyTextButton
|
||||
textToCopy={chain.chainId.toString()}
|
||||
textToShow={chain.chainId.toString()}
|
||||
tooltip="Copy Chain ID"
|
||||
variant="ghost"
|
||||
className="-translate-x-2 py-1 px-2 text-base"
|
||||
iconClassName="size-4"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
</PrimaryInfoItem>
|
||||
|
||||
{/* Native token */}
|
||||
<PrimaryInfoItem title="Native Token">
|
||||
{chain.nativeCurrency.name} ({chain.nativeCurrency.symbol})
|
||||
</PrimaryInfoItem>
|
||||
|
||||
{chain.rpc[0] && <ChainLiveStats rpc={chain.rpc[0]} />}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export function ClaimChainSection() {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Claim Chain" />
|
||||
<div className="border p-4 rounded-lg">
|
||||
<p className="mb-1"> This chain page is unclaimed </p>
|
||||
<p className="text-secondary-foreground text-sm mb-5">
|
||||
Are you the owner of this ecosystem? Claim this page to add additional
|
||||
information on this page to help your developers get started!
|
||||
</p>
|
||||
|
||||
<Button variant="outline" asChild>
|
||||
<Link
|
||||
className="gap-2"
|
||||
href="https://share.hsforms.com/1o01TyfsZRAao2eCrzuXSVgea58c"
|
||||
target="_blank"
|
||||
>
|
||||
<span>Claim Chain</span>
|
||||
<ExternalLinkIcon className="size-4 -mt-[3px]" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
import { FaucetButton } from "../client/FaucetButton";
|
||||
import { GiftIcon } from "../icons/GiftIcon";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
const amountToGive = "0.1";
|
||||
|
||||
export async function FaucetSection(props: { chain: ChainMetadata }) {
|
||||
const { chain } = props;
|
||||
|
||||
// Check eligibilty.
|
||||
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Faucet" />
|
||||
<div className="border border-border px-4 py-10 rounded-lg flex justify-center">
|
||||
<div className="max-w-[520px] w-full flex flex-col items-center ">
|
||||
<div className="flex items-center">
|
||||
<ChainIcon
|
||||
className="size-12 -mr-2 rounded-full border p-1"
|
||||
iconUrl={props.chain.icon?.url}
|
||||
/>
|
||||
<GiftIcon bg="hsl(var(--background))" className="size-12 -ml-2" />
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<h2 className="text-lg tracking-tight font-semibold text-center px-4">
|
||||
{sanitizedChainName} Faucet
|
||||
</h2>
|
||||
|
||||
<div className="h-2" />
|
||||
|
||||
<p className="text-secondary-foreground text-sm max-w-[520px] text-center px-4">
|
||||
Get free {chain.nativeCurrency.symbol} fast and reliably.{" "}
|
||||
{amountToGive} {chain.nativeCurrency.symbol}/day.
|
||||
</p>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
<FaucetButton chain={chain} amount={amountToGive} />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export function SectionTitle(props: { title: string }) {
|
||||
return (
|
||||
<h2 className="text-xl tracking-tight mb-2 font-semibold">{props.title}</h2>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CircleCheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { products } from "../../../../components/server/products";
|
||||
import type { ChainMetadataWithServices } from "../../../../types/chain";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export function SupportedProductsSection(props: {
|
||||
services: ChainMetadataWithServices["services"];
|
||||
}) {
|
||||
const enabledProducts = products.filter((product) => {
|
||||
return props.services.find(
|
||||
(service) => service.service === product.id && service.enabled,
|
||||
);
|
||||
});
|
||||
|
||||
if (enabledProducts.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="thirdweb Products" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{enabledProducts.map((product) => {
|
||||
return (
|
||||
<div
|
||||
key={product.id}
|
||||
className="border rounded-lg p-4 flex gap-3 relative hover:bg-secondary transition-colors duration-300 pr-8"
|
||||
>
|
||||
<CircleCheckIcon className="size-5 text-success-text absolute top-4 right-4" />
|
||||
<product.icon className="size-5 shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h3 className="font-medium mb-1.5">
|
||||
<Link
|
||||
href={product.link}
|
||||
className="before:absolute before:inset-0"
|
||||
target="_blank"
|
||||
>
|
||||
{" "}
|
||||
{product.name}{" "}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-sm text-secondary-foreground">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../../../contexts/map-chains";
|
||||
import { ChainIcon } from "../../../../components/server/chain-icon";
|
||||
import { AddChainToWallet } from "../client/add-chain-to-wallet";
|
||||
|
||||
type ChainHeaderProps = {
|
||||
headerImageUrl?: string;
|
||||
logoUrl?: string;
|
||||
chain: ChainMetadata;
|
||||
};
|
||||
|
||||
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
|
||||
|
||||
export function ChainHeader(props: ChainHeaderProps) {
|
||||
return (
|
||||
// force the banner image to be 4:1 aspect ratio and full-width on mobile devices
|
||||
<div className="flex flex-col">
|
||||
{!props.headerImageUrl && <div className="h-8 md:hidden" />}
|
||||
|
||||
<AspectRatio
|
||||
ratio={props.headerImageUrl ? 4 : 8}
|
||||
className="border-b border-border -mx-4 lg:-mx-6"
|
||||
>
|
||||
{props.headerImageUrl && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={props.headerImageUrl}
|
||||
alt=""
|
||||
className="object-cover object-center h-full w-full"
|
||||
/>
|
||||
)}
|
||||
</AspectRatio>
|
||||
|
||||
{/* below header */}
|
||||
<div className="relative flex flex-row justify-end items-end">
|
||||
{/* chain logo */}
|
||||
|
||||
<ChainIcon
|
||||
iconUrl={props.logoUrl}
|
||||
className="p-2 lg:p-4 absolute top-0 left-0 size-20 lg:size-36 rounded-full bg-background -translate-y-[50%] overflow-hidden border border-border"
|
||||
/>
|
||||
|
||||
{/* action group */}
|
||||
<div className="pt-3 lg:pt-6">
|
||||
{/* Desktop only */}
|
||||
<div className="hidden lg:flex flex-row gap-2">
|
||||
<AddChainToWallet
|
||||
chain={
|
||||
// Do not include chain overrides for chain pages
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
mapV4ChainToV5Chain(props.chain)
|
||||
}
|
||||
/>
|
||||
<Button variant="primary" asChild>
|
||||
<Link href="https://thirdweb.com/dashboard" target="_blank">
|
||||
Get started with thirdweb
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,36 +11,33 @@ export type ChainCTAProps = {
|
||||
|
||||
export function ChainCTA(props: ChainCTAProps) {
|
||||
return (
|
||||
<div className="container">
|
||||
<div
|
||||
className="w-full overflow-hidden rounded-xl shadow-lg bg-cover bg-center bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url(${props.backgroundImageUrl})`,
|
||||
}}
|
||||
>
|
||||
<div className="grid items-center justify-center gap-6 bg-gradient-to-r from-accent/0 to-accent/0 p-8 sm:p-12 md:grid-cols-[1fr_auto] md:gap-12 lg:p-16">
|
||||
<div className="space-y-4 text-center md:text-left">
|
||||
<h2 className="text-3xl font-bold tracking-tighter text-white sm:text-4xl md:text-5xl">
|
||||
{props.title}
|
||||
</h2>
|
||||
{props.description && (
|
||||
<p className="max-w-[600px] md:text-lg text-white">
|
||||
{props.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button asChild size="lg" className="invert dark:invert-0">
|
||||
<Link
|
||||
href={props.buttonLink}
|
||||
prefetch={false}
|
||||
target={
|
||||
props.buttonLink.startsWith("http") ? "_blank" : undefined
|
||||
}
|
||||
>
|
||||
{props.buttonText}
|
||||
</Link>
|
||||
</Button>
|
||||
<div
|
||||
className="bg-cover bg-center bg-no-repeat rounded-xl border border-border overflow-hidden"
|
||||
style={{
|
||||
backgroundImage: `url(${props.backgroundImageUrl})`,
|
||||
}}
|
||||
>
|
||||
<div className="grid items-center justify-center gap-6 p-8 md:grid-cols-[1fr_auto] md:gap-12 bg-gradient-to-r from-background to-background/90">
|
||||
<div className="space-y-4 text-center md:text-left">
|
||||
<h2 className="text-2xl md:text-3xl font-bold tracking-tighter text-foreground md:max-w-[85%]">
|
||||
{props.title}
|
||||
</h2>
|
||||
{props.description && (
|
||||
<p className="max-w-[600px] text-secondary-foreground leading-relaxed text-sm md:text-base">
|
||||
{props.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link
|
||||
href={props.buttonLink}
|
||||
prefetch={false}
|
||||
target={props.buttonLink.startsWith("http") ? "_blank" : undefined}
|
||||
className="font-semibold"
|
||||
>
|
||||
{props.buttonText}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ChainMetadataWithServices } from "../../../../types/chain";
|
||||
import { SectionTitle } from "./SectionTitle";
|
||||
|
||||
export function ExplorersSection(props: {
|
||||
explorers: NonNullable<ChainMetadataWithServices["explorers"]>;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-base text-muted-foreground mb-2 font-medium">
|
||||
Explorers
|
||||
</h2>
|
||||
<section>
|
||||
<SectionTitle title="Explorers" />
|
||||
<div className="grid grid-cols-1 gap-3 md:gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{props.explorers.map((explorer) => {
|
||||
return (
|
||||
<div
|
||||
key={explorer.url}
|
||||
className="p-4 border rounded-xl relative bg-secondary hover:bg-muted"
|
||||
className="p-4 border rounded-xl relative hover:bg-secondary"
|
||||
>
|
||||
<ExternalLinkIcon className="size-4 absolute top-4 right-4 text-muted-foreground" />
|
||||
<h3 className="mb-1 text-base font-semibold capitalize">
|
||||
{explorer.name}
|
||||
</h3>
|
||||
@@ -29,11 +29,10 @@ export function ExplorersSection(props: {
|
||||
? explorer.url.slice(0, -1)
|
||||
: explorer.url}
|
||||
</Link>
|
||||
<ExternalLinkIcon className="size-4 absolute top-4 right-4 text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export function FaucetsSection(props: { faucets: readonly string[] }) {
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-base text-muted-foreground mb-2 font-medium">
|
||||
Faucets
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 gap-3 md:gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{props.faucets.map((faucet) => {
|
||||
const url = new URL(faucet);
|
||||
const hostnameSplit = url.hostname.split(".");
|
||||
const tld = hostnameSplit.pop();
|
||||
const domain = hostnameSplit.pop();
|
||||
const displayTitle = `${domain}.${tld}`;
|
||||
let displayUrl = url.port + url.hostname + url.pathname;
|
||||
displayUrl = displayUrl.endsWith("/")
|
||||
? displayUrl.slice(0, -1)
|
||||
: displayUrl;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={faucet}
|
||||
className="p-4 border rounded-xl relative bg-secondary hover:bg-muted"
|
||||
>
|
||||
<h3 className="mb-1 text-base font-semibold capitalize">
|
||||
{displayTitle}
|
||||
</h3>
|
||||
<Link
|
||||
href={faucet}
|
||||
target="_blank"
|
||||
className="text-sm flex gap-1.5 items-center before:absolute before:inset-0 before:z-0 text-muted-foreground"
|
||||
>
|
||||
{displayUrl}
|
||||
</Link>
|
||||
<ExternalLinkIcon className="size-4 absolute top-4 right-4 text-muted-foreground" />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ExternalLinkIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
|
||||
export function InfoCard(props: {
|
||||
title: string;
|
||||
links?: { label: string; href: string }[];
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<div className="border rounded-xl px-4 py-4 bg-secondary relative">
|
||||
<h2 className="text-xl font-semibold tracking-tight mb-4">
|
||||
{props.title}
|
||||
</h2>
|
||||
|
||||
<div className="[&_p]:mb-3 [&_p]:text-secondary-foreground max-w-[1000px]">
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
{props.links && (
|
||||
<div className="flex flex-col lg:flex-row mt-7 gap-3">
|
||||
{props.links.map((link) => {
|
||||
return (
|
||||
<Button asChild variant="outline" key={link.label}>
|
||||
<Link
|
||||
href={link.href}
|
||||
className="items-center gap-2"
|
||||
target="_blank"
|
||||
>
|
||||
{link.label}{" "}
|
||||
<ExternalLinkIcon className="size-3 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,14 +4,14 @@ export function PrimaryInfoItem(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<section className="border-b pb-4 lg:pb-6">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-base text-muted-foreground font-medium">
|
||||
<h3 className="text-base text-muted-foreground font-medium">
|
||||
{props.title}
|
||||
</h2>
|
||||
</h3>
|
||||
{props.titleIcon}
|
||||
</div>
|
||||
{props.children}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { FaReact } from "react-icons/fa";
|
||||
import { FaUnity } from "react-icons/fa6";
|
||||
import { SiDotnet, SiSolidity, SiTypescript } from "react-icons/si";
|
||||
import { getChain } from "../../../utils";
|
||||
import { InfoCard } from "../components/server/info-card";
|
||||
|
||||
type SDKInfo = {
|
||||
name: string;
|
||||
href: string;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
};
|
||||
|
||||
const sdks: SDKInfo[] = [
|
||||
{
|
||||
name: "TypeScript",
|
||||
href: "https://portal.thirdweb.com/typescript/v5",
|
||||
icon: SiTypescript,
|
||||
},
|
||||
{
|
||||
name: "React",
|
||||
href: "https://portal.thirdweb.com/typescript/v5/react",
|
||||
icon: FaReact,
|
||||
},
|
||||
{
|
||||
name: "React Native",
|
||||
href: "https://portal.thirdweb.com/typescript/v5/react-native",
|
||||
icon: FaReact,
|
||||
},
|
||||
{
|
||||
name: "Unity",
|
||||
href: "https://portal.thirdweb.com/unity",
|
||||
icon: FaUnity,
|
||||
},
|
||||
{
|
||||
name: "Solidity",
|
||||
href: "https://portal.thirdweb.com/contracts/build/overview",
|
||||
icon: SiSolidity,
|
||||
},
|
||||
{
|
||||
name: ".NET SDK",
|
||||
href: "https://portal.thirdweb.com/dotnet",
|
||||
icon: SiDotnet,
|
||||
},
|
||||
];
|
||||
|
||||
export default async function Page(props: { params: { chain_id: string } }) {
|
||||
const chain = await getChain(props.params.chain_id);
|
||||
const enabled = chain.services.find(
|
||||
(s) => s.service === "connect-sdk",
|
||||
)?.enabled;
|
||||
|
||||
if (!enabled) {
|
||||
redirect(`/${props.params.chain_id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InfoCard
|
||||
title="Connect SDK"
|
||||
links={[
|
||||
{
|
||||
label: "Learn More",
|
||||
href: "https://portal.thirdweb.com/connect",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>
|
||||
Connect is the complete toolkit for connecting every user to your
|
||||
application.
|
||||
</p>
|
||||
<p>
|
||||
It features customizable onboarding flows, self-custodial in-app
|
||||
wallets, account abstraction, onramps, and more.
|
||||
</p>
|
||||
</InfoCard>
|
||||
|
||||
<div className="h-10" />
|
||||
|
||||
<h3 className="text-foreground text-3xl tracking-tighter font-semibold">
|
||||
Get Started
|
||||
</h3>
|
||||
|
||||
<div className="h-3" />
|
||||
|
||||
<div className="grid gap-3 md:gap-4 grid-cols-1 md:grid-cols-3 lg:grid-cols-4">
|
||||
{sdks.map((sdk) => {
|
||||
return (
|
||||
<Link
|
||||
key={sdk.name}
|
||||
href={sdk.href}
|
||||
target="_blank"
|
||||
className="border p-4 bg-secondary rounded-lg hover:bg-muted font-medium flex items-center gap-3"
|
||||
>
|
||||
<sdk.icon className="size-5 text-muted-foreground" />
|
||||
{sdk.name}
|
||||
</Link>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getChain } from "../../../utils";
|
||||
import { InfoCard } from "../components/server/info-card";
|
||||
|
||||
export default async function Page(props: { params: { chain_id: string } }) {
|
||||
const chain = await getChain(props.params.chain_id);
|
||||
const enabled = chain.services.find((s) => s.service === "engine")?.enabled;
|
||||
|
||||
if (!enabled) {
|
||||
redirect(`/${props.params.chain_id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InfoCard
|
||||
title="thirdweb Engine"
|
||||
links={[
|
||||
{
|
||||
label: "Setup Engine Instance",
|
||||
href: "/dashboard/engine",
|
||||
},
|
||||
{
|
||||
label: "Learn More",
|
||||
href: "https://portal.thirdweb.com/engine",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>
|
||||
thirdweb Engine is a backend HTTP server that reads, writes, and
|
||||
deploys contracts at production scale.
|
||||
</p>
|
||||
</InfoCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,205 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
|
||||
import { CustomConnectWallet } from "@3rdweb-sdk/react/components/connect-wallet";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { ChainMetadataWithServices } from "app/(dashboard)/(chain)/types/chain";
|
||||
import { thirdwebClient } from "lib/thirdweb-client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Confetti from "react-confetti";
|
||||
import { toast } from "sonner";
|
||||
import { type ISpinWheelProps, SpinWheel } from "spin-wheel-game";
|
||||
import { defineChain, toUnits } from "thirdweb";
|
||||
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
|
||||
|
||||
const segments = [
|
||||
{ segmentText: "Come back tomorrow", segColor: "#f771c9" }, // Mid-dark, optimized for dark mode
|
||||
{ segmentText: "0.1", segColor: "#f65ac0" }, // Lightest, optimized for dark mode
|
||||
{ segmentText: "Come back tomorrow", segColor: "#f542b7" }, // Slightly darker, optimized
|
||||
{ segmentText: "0.01", segColor: "#f32bae" }, // Mid-light, optimized for dark mode
|
||||
{ segmentText: "0.05", segColor: "#f213a5" }, // Mid, optimized for dark mode
|
||||
{ segmentText: "0.01", segColor: "#da1195" }, // Mid-dark, optimized for dark mode
|
||||
{ segmentText: "0.05", segColor: "#c20f84" }, // Darker, optimized for dark mode
|
||||
{ segmentText: "0.01", segColor: "#a90d73" }, // Even darker, optimized
|
||||
{ segmentText: "0.05", segColor: "#910b63" }, // Near darkest, optimized for dark mode
|
||||
{ segmentText: "0.01", segColor: "#790a53" }, // Lightest, optimized for dark mode
|
||||
{ segmentText: "0.05", segColor: "#610842" }, // Slightly darker, optimized
|
||||
{ segmentText: "0.01", segColor: "#490632" }, // Mid-light, optimized for dark mode
|
||||
];
|
||||
|
||||
function formatTime(seconds: number) {
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
|
||||
if (hours > 0) {
|
||||
return rtf.format(+hours, "hour");
|
||||
}
|
||||
|
||||
if (minutes > 0) {
|
||||
return rtf.format(+minutes, "minute");
|
||||
}
|
||||
|
||||
return rtf.format(+seconds, "second");
|
||||
}
|
||||
|
||||
export function TestnetSpinWheel({
|
||||
chain,
|
||||
ttlSeconds,
|
||||
}: {
|
||||
chain: ChainMetadataWithServices;
|
||||
ttlSeconds: number;
|
||||
}) {
|
||||
const address = useActiveAccount()?.address;
|
||||
const chainId = chain.chainId;
|
||||
const definedChain = defineChain(chainId);
|
||||
const { data: evmBalance, isLoading } = useWalletBalance({
|
||||
address: THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
chain: definedChain,
|
||||
client: thirdwebClient,
|
||||
});
|
||||
const router = useRouter();
|
||||
|
||||
const claimMutation = useMutation({
|
||||
mutationFn: async (result: string) => {
|
||||
const amount = result === "Come back tomorrow" ? "0.01" : result;
|
||||
const requestBody = {
|
||||
chainId: chainId,
|
||||
toAddress: address,
|
||||
amount: amount,
|
||||
};
|
||||
|
||||
const response = await fetch("/api/testnet-faucet/claim", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw error.error;
|
||||
}
|
||||
},
|
||||
onError: async (error: string) => {
|
||||
toast.error(error);
|
||||
},
|
||||
onSuccess: () => {
|
||||
router.refresh();
|
||||
const successMessage =
|
||||
claimMutation.variables === "Come back tomorrow"
|
||||
? "You're not lucky today, but don't worry you got 0.01 💜"
|
||||
: `${claimMutation.variables} ${chain?.nativeCurrency?.name} sent to ${address}`;
|
||||
toast.success(successMessage);
|
||||
},
|
||||
});
|
||||
|
||||
const isFaucetEmpty =
|
||||
evmBalance?.value !== undefined && evmBalance.value < toUnits("1", 17);
|
||||
|
||||
const faucetMessage = isFaucetEmpty ? (
|
||||
<div>
|
||||
<p>
|
||||
There is no {chain.nativeCurrency.symbol} available. Please send funds
|
||||
to the following address:
|
||||
</p>
|
||||
<div className="h-2" />
|
||||
<CopyTextButton
|
||||
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
tooltip="Copy"
|
||||
className="text-lg -translate-x-2 py-0.5"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p>You may claim {chain.nativeCurrency.symbol} once every 24 hours.</p>
|
||||
);
|
||||
|
||||
const claimButtonCta =
|
||||
ttlSeconds > 0
|
||||
? `Your next claim is available ${formatTime(ttlSeconds)}`
|
||||
: `Spin for ${chain.nativeCurrency.symbol}`;
|
||||
const closeButtonCta = ttlSeconds > 0 ? "Done" : "Close";
|
||||
|
||||
const spinWheelProps: ISpinWheelProps = {
|
||||
segments,
|
||||
onFinished: claimMutation.mutateAsync,
|
||||
primaryColor: "black",
|
||||
contrastColor: "white",
|
||||
buttonText: "💜",
|
||||
isOnlyOnce: false,
|
||||
size: 290,
|
||||
// Used to randomize result.
|
||||
upDuration: Math.random() * 100 + 100,
|
||||
downDuration: 600,
|
||||
arrowLocation: "top",
|
||||
showTextOnSpin: false,
|
||||
isSpinSound: false,
|
||||
};
|
||||
|
||||
if (!address) return <CustomConnectWallet />;
|
||||
|
||||
return (
|
||||
<>
|
||||
{isLoading ? (
|
||||
<Skeleton className="h-20" />
|
||||
) : (
|
||||
<>
|
||||
<div className="mb-3">{faucetMessage}</div>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
{!isFaucetEmpty && (
|
||||
<Button className="min-w-48" disabled={ttlSeconds > 0}>
|
||||
{claimButtonCta}
|
||||
</Button>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="w-auto h-fit max-w-none">
|
||||
{claimMutation.isSuccess && (
|
||||
<Confetti className="w-full h-full" />
|
||||
)}
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
Spin to claim {chain.nativeCurrency.symbol} on {chain.name}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
<p className="py-2">
|
||||
Click the wheel to get your claim amount.
|
||||
</p>
|
||||
<div className="p-4">
|
||||
<SpinWheel {...spinWheelProps} />
|
||||
</div>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter className="flex justify-between">
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">
|
||||
<span className="text-muted-foreground">
|
||||
{closeButtonCta}
|
||||
</span>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { THIRDWEB_ENGINE_FAUCET_WALLET } from "@/constants/env";
|
||||
import { ClientOnly } from "components/ClientOnly/ClientOnly";
|
||||
import { getIpAddress } from "lib/ip";
|
||||
import { cacheTtl } from "lib/redis";
|
||||
import { redirect } from "next/navigation";
|
||||
import type { Metadata } from "next/types";
|
||||
import { getChain } from "../../../utils";
|
||||
import { InfoCard } from "../components/server/info-card";
|
||||
import { TestnetSpinWheel } from "./components/SpinWheel";
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: { params: { chain_id: string } }): Promise<Metadata> {
|
||||
const chain = await getChain(params.chain_id);
|
||||
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
|
||||
|
||||
return {
|
||||
title: `thirdweb ${sanitizedChainName} faucet`,
|
||||
description: `Get ${chain.nativeCurrency.symbol} testnet funds on ${chain.name}. Powered by thirdweb.`,
|
||||
};
|
||||
}
|
||||
|
||||
export default async function Page(props: { params: { chain_id: string } }) {
|
||||
const chain = await getChain(props.params.chain_id);
|
||||
|
||||
if (!chain.testnet) {
|
||||
redirect(`/${props.params.chain_id}`);
|
||||
}
|
||||
|
||||
// Check eligibilty.
|
||||
const ipAddress = getIpAddress();
|
||||
const cacheKey = `testnet-faucet:${chain.chainId}:${ipAddress}`;
|
||||
const ttlSeconds = await cacheTtl(cacheKey);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="items-center">
|
||||
<h3 className="mb-3 text-lg font-semibold"> Faucet </h3>
|
||||
<ClientOnly ssr={null}>
|
||||
<TestnetSpinWheel chain={chain} ttlSeconds={ttlSeconds} />
|
||||
</ClientOnly>
|
||||
|
||||
<div className="h-10" />
|
||||
|
||||
<InfoCard title="Resources">
|
||||
<h3 className="mb-2 text-lg font-semibold">What is a faucet?</h3>
|
||||
<p>
|
||||
An testnet faucet is an online service that provides free testnet
|
||||
currency to web3 app and blockchain developers. This allows them to
|
||||
experiment with and test smart contracts and decentralized
|
||||
applications (dApps) on EVM testnets like {chain.name} without using
|
||||
real cryptocurrency.
|
||||
</p>
|
||||
|
||||
<h2>How often can I claim from the faucet?</h2>
|
||||
<p>You may claim funds once per testnet chain every 24 hours.</p>
|
||||
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
How can I refill the faucet?
|
||||
</h3>
|
||||
<p>Please send funds the following address:</p>
|
||||
<div>
|
||||
<CopyTextButton
|
||||
textToCopy={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
textToShow={THIRDWEB_ENGINE_FAUCET_WALLET}
|
||||
tooltip="Copy"
|
||||
copyIconPosition="right"
|
||||
className="mb-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 className="mb-2 text-lg font-semibold">
|
||||
How does the faucet work?
|
||||
</h3>
|
||||
<p>
|
||||
When you request testnet funds, a multi-chain backend wallet
|
||||
transfers a small amount of {chain.nativeCurrency.symbol} on{" "}
|
||||
{chain.name} with{" "}
|
||||
<a
|
||||
href="https://thirdweb.com/engine"
|
||||
target="_blank"
|
||||
className="underline"
|
||||
rel="noreferrer"
|
||||
>
|
||||
thirdweb Engine
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</InfoCard>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,36 +1,23 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { BurgerMenuButton } from "@/components/blocks/BurgerMenuButton";
|
||||
import { Sidebar } from "@/components/blocks/Sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
ArrowLeftIcon,
|
||||
CircleAlertIcon,
|
||||
ExternalLinkIcon,
|
||||
TicketCheckIcon,
|
||||
} from "lucide-react";
|
||||
import { ArrowLeftIcon, TicketCheckIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../contexts/map-chains";
|
||||
import { StarButton } from "../../components/client/star-button";
|
||||
import { ChainIcon } from "../../components/server/chain-icon";
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { AddChainToWallet } from "./components/client/add-chain-to-wallet";
|
||||
import { ChainPageTabs } from "./components/client/tabs";
|
||||
import { ChainCTA } from "./components/server/cta-card";
|
||||
import { ExplorersSection } from "./components/server/explorer-section";
|
||||
import { PrimaryInfoItem } from "./components/server/primary-info-item";
|
||||
import { ChainHeader } from "./components/server/chain-header";
|
||||
|
||||
export async function generateMetadata(
|
||||
{ params }: { params: { chain_id: string } },
|
||||
// parent: ResolvingMetadata,
|
||||
): Promise<Metadata> {
|
||||
// read route params
|
||||
// TODO: improve the behavior when clicking "Get started with thirdweb", currently just redirects to the dashboard
|
||||
|
||||
export async function generateMetadata({
|
||||
params,
|
||||
}: { params: { chain_id: string } }): Promise<Metadata> {
|
||||
const chain = await getChain(params.chain_id);
|
||||
|
||||
const sanitizedChainName = chain.name.replace("Mainnet", "").trim();
|
||||
|
||||
const title = `${sanitizedChainName}: RPC and Chain Settings`;
|
||||
|
||||
const description = `Use the best ${sanitizedChainName} RPC and add to your wallet. Discover the chain ID, native token, explorers, and ${
|
||||
@@ -53,226 +40,137 @@ export default async function ChainPageLayout({
|
||||
}) {
|
||||
const chain = await getChain(params.chain_id);
|
||||
|
||||
// Enable faucet for all testnet chains.
|
||||
if (chain.testnet) {
|
||||
chain.services = [
|
||||
...chain.services,
|
||||
{
|
||||
service: "faucet",
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
if (params.chain_id !== chain.slug) {
|
||||
redirect(chain.slug);
|
||||
}
|
||||
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
// true if we *have* chainMetadata for the chain
|
||||
const isVerified = !!chainMetadata;
|
||||
const isDeprecated = chain.status === "deprecated";
|
||||
|
||||
const sidebarLinks = [
|
||||
{
|
||||
href: `/${chain.slug}`,
|
||||
label: "Overview",
|
||||
},
|
||||
{
|
||||
href: `/${chain.slug}/popular`,
|
||||
label: "Popular Contracts",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isDeprecated && (
|
||||
<>
|
||||
<div className="bg-destructive">
|
||||
<div className="container py-4 flex flex-row items-center gap-4 text-white">
|
||||
<CircleAlertIcon className="size-6 flex-shrink-0" />
|
||||
<h3 className="font-semibold">
|
||||
This chain has been marked as deprecated. Some or all services
|
||||
may no longer function.
|
||||
</h3>
|
||||
</div>
|
||||
<div className="container flex flex-row h-full">
|
||||
<Sidebar
|
||||
header={
|
||||
<div className="flex items-center gap-1.5 mb-5 flex-wrap">
|
||||
<Link
|
||||
href="/chainlist"
|
||||
className="text-secondary-foreground hover:text-foreground text-sm"
|
||||
>
|
||||
Chainlist
|
||||
</Link>
|
||||
<span className="text-secondary-foreground text-sm"> / </span>
|
||||
<Link href={`/${chain.slug}`} className="text-foreground text-sm">
|
||||
{chain.name.replace("Mainnet", "")}
|
||||
</Link>
|
||||
</div>
|
||||
<Separator />
|
||||
</>
|
||||
)}
|
||||
<section className="flex flex-col h-full gap-8">
|
||||
{/* Header */}
|
||||
<header
|
||||
className={cn(
|
||||
"py-6 md:py-8 border-b relative overflow-hidden",
|
||||
!chainMetadata?.gasSponsored && !isVerified && "md:pb-2",
|
||||
chainMetadata?.headerImgUrl && "md:py-10",
|
||||
}
|
||||
links={sidebarLinks}
|
||||
/>
|
||||
|
||||
<div className="flex flex-col w-full lg:px-6 lg:border-x pb-10">
|
||||
{/* Icon + Background */}
|
||||
<ChainHeader
|
||||
headerImageUrl={chainMetadata?.headerImgUrl}
|
||||
logoUrl={chain.icon?.url}
|
||||
chain={chain}
|
||||
/>
|
||||
|
||||
<div className="flex lg:hidden justify-end">
|
||||
<BurgerMenuButton
|
||||
links={sidebarLinks}
|
||||
footer={
|
||||
<div className="mt-5 border-t pt-6 pb-2">
|
||||
<Link
|
||||
href="/chainlist"
|
||||
className="px-2 text-sm flex justify-start gap-2 items-center text-secondary-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
View all chains
|
||||
</Link>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-4 md:h-8" />
|
||||
|
||||
<div className="flex flex-col gap-3 md:gap-2">
|
||||
{/* Gas Sponsored badge - Mobile */}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
<div className="md:hidden flex">
|
||||
<GasSponseredBadge />
|
||||
</div>
|
||||
)}
|
||||
>
|
||||
{/* header background image shenanigans */}
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0 -z-10">
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 bottom-0 bg-cover bg-center bg-no-repeat bg-secondary"
|
||||
style={
|
||||
chainMetadata?.headerImgUrl
|
||||
? {
|
||||
backgroundImage: `url(${chainMetadata.headerImgUrl})`,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
|
||||
{/* Chain name */}
|
||||
<div className="flex flex-row gap-1.5 items-center">
|
||||
<h1 className="text-2xl font-semibold lg:text-3xl lg:font-semibold tracking-tight">
|
||||
{chain.name}
|
||||
</h1>
|
||||
|
||||
{/* Favorite */}
|
||||
<StarButton
|
||||
chainId={chain.chainId}
|
||||
iconClassName="size-5"
|
||||
className="p-1"
|
||||
/>
|
||||
<div className="absolute top-0 left-0 right-0 bottom-0 bg-ba bg-gradient-to-b from-black/25 to-black/0 shadow-inner" />
|
||||
</div>
|
||||
|
||||
{/* end header shaningans */}
|
||||
<div className="container flex flex-col md:flex-row justify-between md:items-center">
|
||||
<div className="flex flex-col gap-2 md:gap-6">
|
||||
<Link
|
||||
href="/chainlist"
|
||||
className={cn(
|
||||
"text-foreground mt-4 inline-flex items-center gap-1",
|
||||
// if we have a header image always light text
|
||||
chainMetadata?.headerImgUrl && "text-white",
|
||||
)}
|
||||
>
|
||||
<ArrowLeftIcon className="size-5" />
|
||||
Chainlist
|
||||
</Link>
|
||||
|
||||
<div className="flex gap-3 md:gap-5 items-center">
|
||||
{chain.icon?.url && (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-secondary p-2 border rounded-full overflow-hidden",
|
||||
)}
|
||||
>
|
||||
<ChainIcon
|
||||
iconUrl={chain.icon.url}
|
||||
className={cn(
|
||||
"size-14 md:size-18 rounded-full",
|
||||
// if we it's "mantle" chain, invert the icon in dark mode
|
||||
chain.chainId === 5000 && "dark:invert",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chain Name */}
|
||||
<h1
|
||||
className={cn(
|
||||
"font-semibold tracking-tighter text-4xl md:text-6xl",
|
||||
// if we have a header image always light text
|
||||
chainMetadata?.headerImgUrl && "text-white",
|
||||
)}
|
||||
>
|
||||
{chain.name}
|
||||
</h1>
|
||||
<StarButton chainId={chain.chainId} variant="secondary" />
|
||||
{/* Gas Sponsored badge - Desktop */}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
<div className="hidden md:block">
|
||||
<GasSponseredBadge />
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-2 md:gap-4 items-center h-8 mb-4 md:mb-6">
|
||||
{chainMetadata?.gasSponsored && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-accent-foreground pointer-events-none flex flex-row items-center h-full gap-1.5 border border-border"
|
||||
>
|
||||
<TicketCheckIcon className="size-5" />
|
||||
<span className="font-bold text-xs uppercase">
|
||||
gas sponsored
|
||||
</span>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row md:flex-col gap-4">
|
||||
<AddChainToWallet
|
||||
chain={defineChain(chain)}
|
||||
hasBackground={!!chainMetadata?.headerImgUrl}
|
||||
/>
|
||||
{isVerified ? null : (
|
||||
<Button
|
||||
className="w-full md:min-w-40"
|
||||
variant="outline"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className="gap-2"
|
||||
href="https://share.hsforms.com/1o01TyfsZRAao2eCrzuXSVgea58c"
|
||||
target="_blank"
|
||||
>
|
||||
<span>Claim this chain</span>
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{chainMetadata?.cta && <ChainCTA {...chainMetadata.cta} />}
|
||||
|
||||
<main className="container pb-20 flex-1">
|
||||
{/* About section */}
|
||||
{chainMetadata?.about && (
|
||||
<>
|
||||
<div className="border rounded-xl px-4 py-4 bg-card relative">
|
||||
<h2 className="text-xl font-semibold tracking-tight mb-4">
|
||||
About
|
||||
</h2>
|
||||
|
||||
<div className="[&_p]:mb-3 [&_p]:text-card-foreground">
|
||||
<pre className="text-wrap font-sans">
|
||||
{chainMetadata.about}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-8" />
|
||||
</>
|
||||
)}
|
||||
{/* Chain Overview */}
|
||||
<div className="flex flex-col gap-10 pt-2">
|
||||
{/* Info Grid */}
|
||||
<div className="grid grid-cols-1 gap-4 lg:gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
{/* Info */}
|
||||
{chain.infoURL && (
|
||||
<PrimaryInfoItem title="Info">
|
||||
<div className="flex items-center gap-1.5 hover:text-primary">
|
||||
<Link
|
||||
href={chain.infoURL}
|
||||
target="_blank"
|
||||
className="text-lg"
|
||||
>
|
||||
{new URL(chain.infoURL).hostname}
|
||||
</Link>
|
||||
<ExternalLinkIcon className="size-4" />
|
||||
</div>
|
||||
</PrimaryInfoItem>
|
||||
)}
|
||||
|
||||
{/* Chain Id */}
|
||||
<PrimaryInfoItem title="Chain ID">
|
||||
<CopyTextButton
|
||||
textToCopy={chain.chainId.toString()}
|
||||
textToShow={chain.chainId.toString()}
|
||||
tooltip="Copy Chain ID"
|
||||
variant="ghost"
|
||||
className="text-lg -translate-x-2 py-0.5"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
</PrimaryInfoItem>
|
||||
|
||||
{/* Native token */}
|
||||
<PrimaryInfoItem title="Native Token">
|
||||
<div className="text-lg">
|
||||
{chain.nativeCurrency.name} ({chain.nativeCurrency.symbol})
|
||||
</div>
|
||||
</PrimaryInfoItem>
|
||||
</div>
|
||||
|
||||
{/* Explorers */}
|
||||
{chain.explorers && chain.explorers.length > 0 && (
|
||||
<ExplorersSection explorers={chain.explorers} />
|
||||
)}
|
||||
</div>
|
||||
<div className="h-14" />
|
||||
<ChainPageTabs
|
||||
chainSlug={params.chain_id}
|
||||
enabledServices={chain.services
|
||||
.filter((s) => s.enabled)
|
||||
.map((s) => s.service)}
|
||||
/>
|
||||
<div className="h-8" />
|
||||
<div>{children}</div>
|
||||
</main>
|
||||
</section>
|
||||
</>
|
||||
|
||||
{/* description */}
|
||||
{chainMetadata?.about && (
|
||||
<p className="text-secondary-foreground text-sm lg:text-base mb-2">
|
||||
{chainMetadata.about}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* mobile action row */}
|
||||
<div className="sm:hidden w-full">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<AddChainToWallet
|
||||
chain={
|
||||
// Do not include chain overrides for chain pages
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
mapV4ChainToV5Chain(chain)
|
||||
}
|
||||
/>
|
||||
<Button variant="primary">
|
||||
<Link href="https://thirdweb.com/dashboard" target="_blank">
|
||||
Get started
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-8" />
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GasSponseredBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-full text-[hsl(334,81.12%,69.65%)] bg-[hsla(335,57%,51%,0.2)] px-2.5 py-1">
|
||||
<TicketCheckIcon className="size-4" />
|
||||
<span className="font-medium text-xs">Gas Sponsored</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,166 +1,100 @@
|
||||
import { ChevronRight, ShieldCheckIcon } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { type SortBy, fetchTopContracts } from "lib/search";
|
||||
import { ArrowRightIcon, CircleAlertIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
type SortBy,
|
||||
type TimeRange,
|
||||
fetchTopContracts,
|
||||
} from "../../../../../lib/search";
|
||||
import { TrendingContractSection } from "../../../trending/components/trending-table";
|
||||
import { getChain } from "../../utils";
|
||||
import { InfoCard } from "./components/server/info-card";
|
||||
import twPublisherImage from "./tw-publisher.png";
|
||||
|
||||
type ContractCardInfo = {
|
||||
name: string;
|
||||
description: string;
|
||||
href: string;
|
||||
};
|
||||
|
||||
// currently the metadata is hardcoded - TODO:fetch it
|
||||
|
||||
const popularContracts: ContractCardInfo[] = [
|
||||
{
|
||||
name: "NFT Drop",
|
||||
description: "Release collection of unique NFTs for a set price",
|
||||
href: "/thirdweb.eth/DropERC721",
|
||||
},
|
||||
{
|
||||
name: "NFT Collection",
|
||||
description: "Create a collection of unique NFTs",
|
||||
href: "/thirdweb.eth/TokenERC721",
|
||||
},
|
||||
{
|
||||
name: "Edition Drop",
|
||||
description: "Release ERC1155 tokens for a set price",
|
||||
href: "/thirdweb.eth/DropERC1155",
|
||||
},
|
||||
{
|
||||
name: "Token",
|
||||
description: "Create cryptocurrency compliant with ERC20 standard",
|
||||
href: "/thirdweb.eth/TokenERC20",
|
||||
},
|
||||
{
|
||||
name: "Edition",
|
||||
description: "Create editions of ERC1155 tokens",
|
||||
href: "/thirdweb.eth/TokenERC1155",
|
||||
},
|
||||
];
|
||||
import { getChain, getChainMetadata } from "../../utils";
|
||||
import { BuyFundsSection } from "./components/server/BuyFundsSection";
|
||||
import { ChainOverviewSection } from "./components/server/ChainOverviewSection";
|
||||
import { ClaimChainSection } from "./components/server/ClaimChainSection";
|
||||
import { FaucetSection } from "./components/server/FaucetSection";
|
||||
import { SupportedProductsSection } from "./components/server/SupportedProductsSection";
|
||||
import { ChainCTA } from "./components/server/cta-card";
|
||||
import { ExplorersSection } from "./components/server/explorer-section";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: { chain_id: string };
|
||||
searchParams: { timeRange?: TimeRange; page?: number; sortBy?: SortBy };
|
||||
searchParams: { page?: number; sortBy?: SortBy };
|
||||
}) {
|
||||
const chain = await getChain(props.params.chain_id);
|
||||
const chainMetadata = await getChainMetadata(chain.chainId);
|
||||
const isDeprecated = chain.status === "deprecated";
|
||||
|
||||
const topContracts = await fetchTopContracts({
|
||||
chainId: chain.chainId,
|
||||
timeRange: props.searchParams.timeRange,
|
||||
page: props.searchParams.page,
|
||||
sortBy: props.searchParams.sortBy,
|
||||
perPage: 3,
|
||||
timeRange: "month",
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="h-4" />
|
||||
|
||||
{topContracts.length > 0 && (
|
||||
<>
|
||||
<div className="flex flex-col gap-4">
|
||||
<h3 className="text-foreground text-2xl tracking-tighter font-semibold">
|
||||
Popular Contracts on {chain.name.split(" ")[0]}
|
||||
</h3>
|
||||
<TrendingContractSection
|
||||
topContracts={topContracts}
|
||||
chainId={chain.chainId}
|
||||
searchParams={props.searchParams}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-10" />
|
||||
</>
|
||||
<div className="flex flex-col gap-10">
|
||||
{/* Custom CTA */}
|
||||
{(chainMetadata?.cta?.title || chainMetadata?.cta?.description) && (
|
||||
<ChainCTA {...chainMetadata.cta} />
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-foreground text-2xl tracking-tighter font-semibold">
|
||||
Deploy Contracts on {chain.name.split(" ")[0]}
|
||||
</h3>
|
||||
<Link
|
||||
href="/explore"
|
||||
className="text-link-foreground inline-flex items-center gap-1 text-base hover:text-foreground font-medium"
|
||||
>
|
||||
View All
|
||||
<ChevronRight className="size-5" />
|
||||
</Link>
|
||||
</div>
|
||||
{/* Deprecated Alert */}
|
||||
{isDeprecated && (
|
||||
<section className="bg-destructive rounded-lg text-destructive-foreground border border-destructive-foreground/20">
|
||||
<div className="container py-4 flex flex-row items-center gap-4">
|
||||
<CircleAlertIcon className="size-6 flex-shrink-0" />
|
||||
<h3 className="font-medium">
|
||||
This chain has been marked as deprecated. Some or all services may
|
||||
no longer function.
|
||||
</h3>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="h-3" />
|
||||
{/* Buy Funds */}
|
||||
{chain.services.find((c) => c.service === "pay" && c.enabled) && (
|
||||
<BuyFundsSection chain={chain} />
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3 md:gap-4">
|
||||
{popularContracts.map((c) => {
|
||||
return (
|
||||
<ContractCard
|
||||
key={c.name}
|
||||
name={c.name}
|
||||
description={c.description}
|
||||
href={c.href}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* Faucet */}
|
||||
{chain.testnet && <FaucetSection chain={chain} />}
|
||||
|
||||
<div className="h-10" />
|
||||
{/* Chain Overview */}
|
||||
<ChainOverviewSection chain={chain} />
|
||||
|
||||
<InfoCard
|
||||
title="thirdweb Contracts"
|
||||
links={[
|
||||
{
|
||||
label: "Learn More",
|
||||
href: "https://portal.thirdweb.com/contracts",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>End-to-end tools for smart contract development</p>
|
||||
<p>
|
||||
Trusted, modular smart contracts that can be deployed securely on any
|
||||
EVM chain
|
||||
</p>
|
||||
</InfoCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ContractCard(props: ContractCardInfo) {
|
||||
return (
|
||||
<div className="border bg-secondary rounded-xl p-4 hover:bg-muted relative flex flex-col h-full shadow-sm min-h-[200px]">
|
||||
<div className="text-success-text flex items-center gap-1 mb-4 text-sm font-medium">
|
||||
<ShieldCheckIcon className="size-4 text-success-text" />
|
||||
Audited
|
||||
</div>
|
||||
|
||||
<h3 className="text-lg tracking-tight font-semibold text-foreground mb-1">
|
||||
<Link
|
||||
href={props.href}
|
||||
className="before:absolute before:inset-0 before:z-0"
|
||||
>
|
||||
{props.name}
|
||||
</Link>
|
||||
</h3>
|
||||
|
||||
<p className="text-base text-muted-foreground mb-5">
|
||||
{props.description}
|
||||
</p>
|
||||
|
||||
<Link
|
||||
className="inline-flex items-center gap-1.5 z-10 relative mt-auto text-muted-foreground hover:text-foreground"
|
||||
href="/thirdweb.eth"
|
||||
>
|
||||
<Image
|
||||
src={twPublisherImage}
|
||||
alt="thirdweb.eth"
|
||||
className="size-4 rounded-full"
|
||||
/>
|
||||
<p className="text-sm">thirdweb.eth</p>
|
||||
</Link>
|
||||
{/* Explorers */}
|
||||
{chain.explorers && chain.explorers.length > 0 && (
|
||||
<ExplorersSection explorers={chain.explorers} />
|
||||
)}
|
||||
|
||||
{topContracts.length > 0 && (
|
||||
<section>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-xl tracking-tight font-semibold">
|
||||
Popular Contracts
|
||||
</h2>
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={`/${chain.slug}/popular`}
|
||||
className="flex gap-2 items-center"
|
||||
>
|
||||
See All
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<TrendingContractSection
|
||||
topContracts={topContracts}
|
||||
chainId={chain.chainId}
|
||||
searchParams={props.searchParams}
|
||||
showPagination={false}
|
||||
/>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{chain.services.filter((s) => s.enabled).length > 0 && (
|
||||
<SupportedProductsSection services={chain.services} />
|
||||
)}
|
||||
|
||||
{/* Claim Chain */}
|
||||
{!chainMetadata && <ClaimChainSection />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { thirdwebClient } from "@/constants/client";
|
||||
import { useTheme } from "next-themes";
|
||||
import { useMemo } from "react";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { PayEmbed } from "thirdweb/react";
|
||||
|
||||
export function CustomPayEmbed(props: { chainId: number }) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const theme = resolvedTheme === "dark" ? "dark" : "light";
|
||||
const chain = useMemo(() => defineChain(props.chainId), [props.chainId]);
|
||||
return (
|
||||
<PayEmbed
|
||||
client={thirdwebClient}
|
||||
connectOptions={{ chain }}
|
||||
theme={theme}
|
||||
payOptions={{
|
||||
prefillBuy: {
|
||||
chain,
|
||||
allowEdits: { chain: false, amount: true, token: true },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getChain } from "../../../utils";
|
||||
import { InfoCard } from "../components/server/info-card";
|
||||
import { CustomPayEmbed } from "./components/pay-embed.client";
|
||||
|
||||
export default async function Page(props: { params: { chain_id: string } }) {
|
||||
const chain = await getChain(props.params.chain_id);
|
||||
const enabled = chain.services.find((s) => s.service === "pay")?.enabled;
|
||||
|
||||
if (!enabled) {
|
||||
redirect(`/${props.params.chain_id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pb-20 flex flex-col xl:flex-row gap-8">
|
||||
<CustomPayEmbed chainId={chain.chainId} />
|
||||
<InfoCard
|
||||
title="thirdweb Pay"
|
||||
links={[
|
||||
{
|
||||
label: "Get Started",
|
||||
href: "https://portal.thirdweb.com/connect/pay/overview",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>
|
||||
thirdweb Pay allows your users to purchase cryptocurrencies and
|
||||
execute transactions with their credit/debit card, or with any token
|
||||
via cross-chain routing.
|
||||
</p>
|
||||
</InfoCard>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { type SortBy, fetchTopContracts } from "lib/search";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { TrendingContractSection } from "../../../../trending/components/trending-table";
|
||||
import { getChain } from "../../../utils";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: { chain_id: string };
|
||||
searchParams: { page?: number; sortBy?: SortBy };
|
||||
}) {
|
||||
const chain = await getChain(props.params.chain_id);
|
||||
const topContracts = await fetchTopContracts({
|
||||
chainId: chain.chainId,
|
||||
page: props.searchParams.page,
|
||||
sortBy: props.searchParams.sortBy,
|
||||
perPage: 15,
|
||||
timeRange: "month",
|
||||
});
|
||||
|
||||
return (
|
||||
<section className="mt-2">
|
||||
<h2 className="text-2xl tracking-tighter font-semibold mb-2">
|
||||
Popular Contracts
|
||||
</h2>
|
||||
|
||||
<p className="text-sm text-secondary-foreground">
|
||||
Explore contracts on Ethereum and sort them by your preferred metrics
|
||||
</p>
|
||||
|
||||
<div className="h-8" />
|
||||
|
||||
{topContracts.length > 0 && (
|
||||
<TrendingContractSection
|
||||
topContracts={topContracts}
|
||||
chainId={chain.chainId}
|
||||
searchParams={props.searchParams}
|
||||
showPagination={true}
|
||||
/>
|
||||
)}
|
||||
|
||||
{topContracts.length === 0 && (
|
||||
<div className="text-secondary-foreground text-lg flex border rounded-lg h-[200px] border-border items-center justify-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<CircleAlertIcon className="size-5 text-destructive-text" />
|
||||
No contracts found
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { getChain } from "../../../utils";
|
||||
import { InfoCard } from "../components/server/info-card";
|
||||
import { ChainLiveStats } from "./components/client/live-stats";
|
||||
|
||||
export default async function Page(props: { params: { chain_id: string } }) {
|
||||
const chain = await getChain(props.params.chain_id);
|
||||
const enabled = chain.services.find((s) => s.service === "rpc-edge")?.enabled;
|
||||
|
||||
if (!enabled) {
|
||||
redirect(`/${props.params.chain_id}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="pb-20">
|
||||
<InfoCard
|
||||
title="thirdweb RPC Edge"
|
||||
links={[
|
||||
{
|
||||
label: "Learn More",
|
||||
href: "https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
|
||||
},
|
||||
]}
|
||||
>
|
||||
<p>
|
||||
Remote Procedure Call (RPC) Edge provides reliable access to querying
|
||||
data and interacting with the blockchain through global edge RPCs
|
||||
</p>
|
||||
|
||||
<p>
|
||||
By default, thirdweb provides publicly available RPCs for over 1000+
|
||||
EVM Networks
|
||||
</p>
|
||||
</InfoCard>
|
||||
|
||||
{chain.rpc[0] && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mt-10 gap-4">
|
||||
<ChainLiveStats rpc={chain.rpc[0]} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// handle deprecated route
|
||||
// redirect /<chain_id>/faucet to /<chain_id>
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Page(props: { params: { chain_id: string } }) {
|
||||
redirect(`/${props.params.chain_id}`);
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { IPFS_GATEWAY_URL } from "@/constants/env";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY, PROD_OR_DEV_URL } from "constants/rpc";
|
||||
import {
|
||||
ZERO_ADDRESS,
|
||||
createThirdwebClient,
|
||||
defineChain,
|
||||
toTokens,
|
||||
} from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "constants/rpc";
|
||||
import { ZERO_ADDRESS, createThirdwebClient, toTokens } from "thirdweb";
|
||||
import {
|
||||
eth_getBlockByHash,
|
||||
eth_getTransactionByHash,
|
||||
@@ -16,18 +10,9 @@ import {
|
||||
getRpcClient,
|
||||
} from "thirdweb/rpc";
|
||||
import { hexToNumber, shortenAddress, toEther } from "thirdweb/utils";
|
||||
import { mapV4ChainToV5Chain } from "../../../../../../contexts/map-chains";
|
||||
import { getChain } from "../../../utils";
|
||||
|
||||
function defineDashboardChain(chainId: number, dashboardChain?: ChainMetadata) {
|
||||
return defineChain({
|
||||
id: chainId,
|
||||
rpc:
|
||||
dashboardChain?.rpc?.[0] || `https://${chainId}.rpc.${PROD_OR_DEV_URL}`,
|
||||
slug: dashboardChain?.slug,
|
||||
nativeCurrency: dashboardChain?.nativeCurrency,
|
||||
});
|
||||
}
|
||||
|
||||
export default async function Page(props: {
|
||||
params: { chain_id: string; txHash: `0x${string}` };
|
||||
}) {
|
||||
@@ -43,7 +28,9 @@ export default async function Page(props: {
|
||||
|
||||
const rpcRequest = getRpcClient({
|
||||
client: thirdwebClient,
|
||||
chain: defineDashboardChain(chain.chainId),
|
||||
// Do not include chain overrides for chain pages
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: mapV4ChainToV5Chain(chain),
|
||||
});
|
||||
|
||||
const [transaction, receipt] = await Promise.all([
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { PaginationButtons } from "@/components/pagination-buttons";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
import { PaginationButtons } from "../../../../../../@/components/pagination-buttons";
|
||||
|
||||
type ChainlistPaginationProps = {
|
||||
totalPages: number;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Grid2X2Icon, ListIcon } from "lucide-react";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
import { Button } from "../../../../../../@/components/ui/button";
|
||||
|
||||
type hainListViewProps = {
|
||||
activeView: "grid" | "table";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { CircleAlertIcon, TicketCheckIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { CopyTextButton } from "../../../../../../@/components/ui/CopyTextButton";
|
||||
import { ChainIcon } from "../../../components/server/chain-icon";
|
||||
import type { ChainSupportedService } from "../../../types/chain";
|
||||
import { getChainMetadata } from "../../../utils";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { CopyTextButton } from "../../../../../../@/components/ui/CopyTextButton";
|
||||
import { ChainIcon } from "../../../components/server/chain-icon";
|
||||
import { products } from "../../../components/server/products";
|
||||
import type { ChainSupportedService } from "../../../types/chain";
|
||||
@@ -35,7 +35,6 @@ export async function ChainListRow({
|
||||
iconUrl,
|
||||
}: ChainListRowProps) {
|
||||
const chainMetadata = await getChainMetadata(chainId);
|
||||
const productsWithoutFaucet = products.filter((p) => p.id !== "faucet");
|
||||
return (
|
||||
<tr className="border-b relative hover:bg-secondary">
|
||||
<TableData>{favoriteButton}</TableData>
|
||||
@@ -82,7 +81,7 @@ export async function ChainListRow({
|
||||
<TableData>
|
||||
<div className="flex flex-row gap-14 items-center w-[520px] ">
|
||||
<div className="flex items-center gap-7 z-10">
|
||||
{productsWithoutFaucet.map((p) => {
|
||||
{products.map((p) => {
|
||||
return (
|
||||
<ProductIcon
|
||||
key={p.name}
|
||||
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
IPFS_GATEWAY_URL,
|
||||
} from "@/constants/env";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { resolveScheme } from "thirdweb/storage";
|
||||
|
||||
const fallbackChainIcon =
|
||||
"data:image/svg+xml;charset=UTF-8,%3csvg width='15' height='14' viewBox='0 0 15 14' fill='none' xmlns='http://www.w3.org/2000/svg'%3e%3cpath d='M7 8.04238e-07C5.1435 8.04238e-07 3.36301 0.737501 2.05025 2.05025C0.7375 3.36301 0 5.1435 0 7C0 7.225 -1.52737e-07 7.445 0.0349998 7.665C0.16385 9.0151 0.68213 10.2988 1.52686 11.3598C2.37158 12.4209 3.50637 13.2137 4.79326 13.642C6.0801 14.0702 7.4637 14.1153 8.7758 13.7719C10.0879 13.4285 11.2719 12.7113 12.184 11.7075C13.0961 10.7038 13.6969 9.4567 13.9135 8.1178C14.1301 6.7789 13.9531 5.406 13.4039 4.16587C12.8548 2.92574 11.9573 1.87184 10.8204 1.13228C9.6835 0.392721 8.3563 -0.000649196 7 8.04238e-07ZM7 1C8.581 1.00137 10.0975 1.62668 11.22 2.74V3.24C9.2438 2.55991 7.0956 2.56872 5.125 3.265C4.96758 3.1116 4.76997 3.00586 4.555 2.96H4.43C4.37 2.75 4.315 2.54 4.27 2.325C4.225 2.11 4.2 1.92 4.175 1.715C5.043 1.24658 6.0137 1.00091 7 1ZM5.5 3.935C7.3158 3.32693 9.2838 3.34984 11.085 4C10.8414 5.2703 10.3094 6.4677 9.53 7.5C9.312 7.4077 9.0707 7.3855 8.8395 7.4366C8.6083 7.4877 8.3988 7.6094 8.24 7.785C8.065 7.685 7.89 7.585 7.74 7.47C6.7307 6.7966 5.8877 5.9023 5.275 4.855C5.374 4.73221 5.4461 4.58996 5.4866 4.43749C5.5271 4.28502 5.5351 4.12575 5.51 3.97L5.5 3.935ZM3.5 2.135C3.5 2.24 3.53 2.35 3.55 2.455C3.595 2.675 3.655 2.89 3.715 3.105C3.52353 3.21838 3.36943 3.38531 3.2717 3.58522C3.17397 3.78513 3.13688 4.00927 3.165 4.23C2.37575 4.7454 1.67078 5.3795 1.075 6.11C1.19455 5.3189 1.47112 4.55966 1.88843 3.87701C2.30575 3.19437 2.85539 2.60208 3.505 2.135H3.5ZM3.5 9.99C3.30481 10.0555 3.13037 10.1714 2.9943 10.3259C2.85822 10.4804 2.76533 10.6681 2.725 10.87H2.405C1.59754 9.9069 1.1146 8.7136 1.025 7.46L1.08 7.365C1.70611 6.3942 2.52463 5.562 3.485 4.92C3.62899 5.0704 3.81094 5.179 4.01162 5.2345C4.2123 5.2899 4.42423 5.2901 4.625 5.235C5.2938 6.3652 6.208 7.3306 7.3 8.06C7.505 8.195 7.715 8.32 7.925 8.44C7.9082 8.6312 7.9391 8.8237 8.015 9C7.1 9.7266 6.0445 10.256 4.915 10.555C4.78401 10.3103 4.57028 10.1201 4.31199 10.0184C4.05369 9.9167 3.76766 9.9102 3.505 10L3.5 9.99ZM7 12.99C5.9831 12.9903 4.98307 12.7304 4.095 12.235L4.235 12.205C4.43397 12.1397 4.61176 12.0222 4.74984 11.8648C4.88792 11.7074 4.98122 11.5158 5.02 11.31C6.2985 10.984 7.4921 10.3872 8.52 9.56C8.7642 9.7027 9.0525 9.75 9.3295 9.6927C9.6064 9.6355 9.8524 9.4778 10.02 9.25C10.7254 9.4334 11.4511 9.5275 12.18 9.53H12.445C11.9626 10.5673 11.1938 11.4451 10.2291 12.0599C9.2643 12.6747 8.144 13.0009 7 13V12.99ZM10.255 8.54C10.2545 8.3304 10.1975 8.1249 10.09 7.945C10.9221 6.8581 11.5012 5.5991 11.785 4.26C12.035 4.37667 12.2817 4.50667 12.525 4.65C13.0749 5.9495 13.1493 7.4012 12.735 8.75C11.9049 8.8142 11.0698 8.7484 10.26 8.555L10.255 8.54Z' fill='%23646D7A'/%3e%3c/svg%3e";
|
||||
"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iOTYiIGhlaWdodD0iOTYiIHZpZXdCb3g9IjAgMCA5NiA5NiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTY4LjE1MTkgNzUuNzM3MkM2Mi4yOTQzIDc5Ljk5MyA1NS4yMzk3IDgyLjI4NTIgNDcuOTk5MyA4Mi4yODUyQzQwLjc1ODkgODIuMjg1MiAzMy43MDQzIDc5Ljk5MyAyNy44NDY2IDc1LjczNzJNNjMuMDI5MSAxNy4xODM3QzY5LjUzNjggMjAuMzU3NyA3NC44NzI2IDI1LjUxMDQgNzguMjcxOCAzMS45MDMzQzgxLjY3MDkgMzguMjk2MiA4Mi45NTkgNDUuNjAxMiA4MS45NTEzIDUyLjc3MTFNMTQuMDQ3NiA1Mi43NzA4QzEzLjAzOTkgNDUuNjAwOCAxNC4zMjggMzguMjk1OSAxNy43MjcxIDMxLjkwM0MyMS4xMjYzIDI1LjUxMDEgMjYuNDYyMSAyMC4zNTczIDMyLjk2OTggMTcuMTgzM000Ni4wNTk4IDI5LjM2NzVMMjkuMzY3MyA0Ni4wNkMyOC42ODg1IDQ2LjczODkgMjguMzQ5IDQ3LjA3ODMgMjguMjIxOCA0Ny40Njk3QzI4LjExIDQ3LjgxNCAyOC4xMSA0OC4xODQ5IDI4LjIyMTggNDguNTI5MkMyOC4zNDkgNDguOTIwNiAyOC42ODg1IDQ5LjI2MDEgMjkuMzY3MyA0OS45MzlMNDYuMDU5OCA2Ni42MzE0QzQ2LjczODcgNjcuMzEwMyA0Ny4wNzgxIDY3LjY0OTcgNDcuNDY5NSA2Ny43NzY5QzQ3LjgxMzggNjcuODg4OCA0OC4xODQ3IDY3Ljg4ODggNDguNTI5IDY3Ljc3NjlDNDguOTIwNCA2Ny42NDk3IDQ5LjI1OTkgNjcuMzEwMyA0OS45Mzg4IDY2LjYzMTRMNjYuNjMxMiA0OS45MzlDNjcuMzEwMSA0OS4yNjAxIDY3LjY0OTUgNDguOTIwNiA2Ny43NzY3IDQ4LjUyOTJDNjcuODg4NiA0OC4xODQ5IDY3Ljg4ODYgNDcuODE0IDY3Ljc3NjcgNDcuNDY5N0M2Ny42NDk1IDQ3LjA3ODMgNjcuMzEwMSA0Ni43Mzg5IDY2LjYzMTIgNDYuMDZMNDkuOTM4OCAyOS4zNjc1QzQ5LjI1OTkgMjguNjg4NyA0OC45MjA0IDI4LjM0OTIgNDguNTI5IDI4LjIyMkM0OC4xODQ3IDI4LjExMDIgNDcuODEzOCAyOC4xMTAyIDQ3LjQ2OTUgMjguMjIyQzQ3LjA3ODEgMjguMzQ5MiA0Ni43Mzg3IDI4LjY4ODcgNDYuMDU5OCAyOS4zNjc1WiIgc3Ryb2tlPSIjNDA0MDQwIiBzdHJva2Utd2lkdGg9IjYuODU3MTQiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIvPgo8L3N2Zz4K";
|
||||
|
||||
export async function ChainIcon(props: {
|
||||
iconUrl?: string;
|
||||
@@ -42,9 +43,15 @@ export async function ChainIcon(props: {
|
||||
<img
|
||||
alt=""
|
||||
src={res?.status === 200 ? resolved : fallbackChainIcon}
|
||||
className={props.className}
|
||||
className={cn("object-contain", props.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return <img alt="" src={fallbackChainIcon} className={props.className} />;
|
||||
return (
|
||||
<img
|
||||
alt=""
|
||||
src={fallbackChainIcon}
|
||||
className={cn("object-contain", props.className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -11,39 +11,48 @@ export const products = [
|
||||
name: "Contracts",
|
||||
id: "contracts",
|
||||
icon: ContractIcon,
|
||||
description: "Pre-built contracts & deployment tools",
|
||||
link: "https://portal.thirdweb.com/contracts",
|
||||
},
|
||||
{
|
||||
name: "Connect SDK",
|
||||
id: "connect-sdk",
|
||||
icon: ConnectSDKIcon,
|
||||
description: "Client-side SDKs for wallets and blockchain interactions",
|
||||
link: "https://portal.thirdweb.com/connect",
|
||||
},
|
||||
{
|
||||
name: "RPC Edge",
|
||||
id: "rpc-edge",
|
||||
icon: RPCIcon,
|
||||
description: "Performant and scalable RPC service",
|
||||
link: "https://portal.thirdweb.com/infrastructure/rpc-edge/overview",
|
||||
},
|
||||
{
|
||||
name: "Engine",
|
||||
id: "engine",
|
||||
icon: EngineIcon,
|
||||
description: "Backend server that reads, writes, and deploys contracts",
|
||||
link: "https://portal.thirdweb.com/engine",
|
||||
},
|
||||
{
|
||||
name: "Account Abstraction",
|
||||
id: "account-abstraction",
|
||||
icon: SmartAccountIcon,
|
||||
description: "Account factory contracts, bundler & paymaster",
|
||||
link: "https://portal.thirdweb.com/connect/account-abstraction/overview",
|
||||
},
|
||||
{
|
||||
name: "Pay",
|
||||
id: "pay",
|
||||
icon: PayIcon,
|
||||
},
|
||||
{
|
||||
name: "Faucet",
|
||||
id: "faucet",
|
||||
icon: PayIcon,
|
||||
description: "Point of sale solution for bridging, onramping & swapping",
|
||||
link: "https://portal.thirdweb.com/connect/pay/overview",
|
||||
},
|
||||
] satisfies Array<{
|
||||
name: string;
|
||||
id: ChainSupportedService;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
description: string;
|
||||
link: string;
|
||||
}>;
|
||||
|
||||
|
After Width: | Height: | Size: 165 KiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 560 KiB |
|
After Width: | Height: | Size: 629 KiB |
|
After Width: | Height: | Size: 49 KiB |
@@ -2,17 +2,28 @@ import "server-only";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
import type { ChainCTAProps } from "./[chain_id]/(chainPage)/components/server/cta-card";
|
||||
import alephZeroBaner from "./temp-assets/AlephZeroBanner.jpg";
|
||||
import alephZeroCTA from "./temp-assets/AlephZeroCTA.jpg";
|
||||
import ancient8Banner from "./temp-assets/ancient8-banner.png";
|
||||
// TEMPORARY
|
||||
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";
|
||||
import thirdwebCTA from "./temp-assets/cta-thirdweb.png";
|
||||
import lumiaCTA from "./temp-assets/lumia-cta.png";
|
||||
import lumiaBanner from "./temp-assets/lumiaBanner.png";
|
||||
import mantleBanner from "./temp-assets/mantle-banner.jpeg";
|
||||
import plumeBanner from "./temp-assets/plumeBanner.png";
|
||||
import plumeCTA from "./temp-assets/plumeCTA.png";
|
||||
import rootStockBanner from "./temp-assets/rootstock-banner.png";
|
||||
import rootStockCTABG from "./temp-assets/rootstock-cta.png";
|
||||
import superpositionBanner from "./temp-assets/superpositionBanner.png";
|
||||
import superpositionCTA from "./temp-assets/superpositionCTA.png";
|
||||
import thirdwebBanner from "./temp-assets/thirdweb-banner.png";
|
||||
import vanarBanner from "./temp-assets/vanar-banner.png";
|
||||
import vanarCTABG from "./temp-assets/vanar-cta.png";
|
||||
import xaiBanner from "./temp-assets/xai-banner.jpg";
|
||||
|
||||
import type { ChainMetadataWithServices } from "./types/chain";
|
||||
|
||||
// END TEMPORARY
|
||||
@@ -166,6 +177,150 @@ const chainMetaRecord = {
|
||||
title: "Fully EVM compatible L1 for Entertainment",
|
||||
},
|
||||
},
|
||||
// lumia Mainnet
|
||||
994873017: {
|
||||
headerImgUrl: lumiaBanner.src,
|
||||
about:
|
||||
"Lumia is a groundbreaking modular layer 2 blockchain designed to integrate real-world assets (RWA) with cross-chain liquidity. A seamless, efficient, and scalable aggregation layer that unifies liquidity from both centralized (CEX) and decentralized exchanges (DEX) to provide builders with immediate access to the biggest markets in Web3.",
|
||||
cta: {
|
||||
backgroundImageUrl: lumiaCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://docs.lumia.org/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
// Superposition Testnet
|
||||
98985: {
|
||||
headerImgUrl: superpositionBanner.src,
|
||||
about:
|
||||
"Superposition is the first blockchain that pays you to use it. It is a DeFi native Layer-3 that focuses on novel incentives and order-flow for growth and value capture for developers and users alike.\n\nSuperpositions includes a native on-chain orderbook with faster execution speeds through Stylus, providing shared and permissionless liquidity for all apps onchain, and Super Assets, which pay yield when you both hold and use them.",
|
||||
cta: {
|
||||
backgroundImageUrl: superpositionCTA.src,
|
||||
title: "The Blockchain that pays you to use it",
|
||||
buttonLink: "https://superposition.so/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
// Aleph 0 Mainnet
|
||||
41455: {
|
||||
headerImgUrl: alephZeroBaner.src,
|
||||
about:
|
||||
"Aleph Zero is a privacy-enhancing blockchain ecosystem boasting a WASM L1 and an EVM L2 that offer instant transactions, the fastest ZK-based privacy features, and a seamless developer experience.",
|
||||
cta: {
|
||||
backgroundImageUrl: alephZeroCTA.src,
|
||||
title: "See how Aleph Zero redefines EVM privacy",
|
||||
buttonLink: "https://zkos.alephzero.org/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
// B3 Mainnet
|
||||
8333: {
|
||||
headerImgUrl: thirdwebBanner.src,
|
||||
about:
|
||||
"Created by a team of Base/Coinbase alumni and OG ETH contributors, B3 is a horizontally scaled gaming ecosystem built on Base. \n\nOur rollup network settles on Base, ensuring B3 transactions inherit the security of the EVM.",
|
||||
cta: {
|
||||
backgroundImageUrl: thirdwebCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://b3.fun/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
//CampNetwork Testnet v2
|
||||
325000: {
|
||||
headerImgUrl: thirdwebBanner.src,
|
||||
about:
|
||||
"Camp Network is the user identity layer across blockchains. Camp aggregates online user data, enabling users to own and monetize their identity while helping teams better understand user behavior.",
|
||||
cta: {
|
||||
backgroundImageUrl: thirdwebCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://www.campnetwork.xyz/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
//Donatuz
|
||||
42026: {
|
||||
headerImgUrl: thirdwebBanner.src,
|
||||
about:
|
||||
"Monetize your passion, your way. Discover the freedom to grow with our creator-focused platform.",
|
||||
cta: {
|
||||
backgroundImageUrl: thirdwebCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://www.donatuz.org/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
//Fuse
|
||||
122: {
|
||||
headerImgUrl: thirdwebBanner.src,
|
||||
about:
|
||||
"Build and grow on the leading web3 platform for business and finance, powered by a performant zkEVM tailor made to get millions of consumers on-chain.",
|
||||
cta: {
|
||||
backgroundImageUrl: thirdwebCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://www.fuse.io/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
//Gemuchain
|
||||
1903648807: {
|
||||
headerImgUrl: thirdwebBanner.src,
|
||||
about:
|
||||
"Is a new Layer 2 integrating Al, Cybersecurity, ZK and a new Consensus layer - Proof Of Longevity. It will protect projects from bad actors and practices including hacks, manipulations, MEV bots and more, penalize bad actors and reward good actors and long term holders, so unlike anything out there today.",
|
||||
cta: {
|
||||
backgroundImageUrl: thirdwebCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://gemuchain.io/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
//0G-Newton-Testnet
|
||||
16600: {
|
||||
headerImgUrl: thirdwebBanner.src,
|
||||
about:
|
||||
"0G is an infinitely scalable data availability layer and data storage system that provides the necessary infrastructure to scale Web3 and bring novel use cases on-chain.",
|
||||
cta: {
|
||||
backgroundImageUrl: thirdwebCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://0g.ai/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
//Plume
|
||||
161221135: {
|
||||
headerImgUrl: plumeBanner.src,
|
||||
about:
|
||||
"Plume is the first modular L2 blockchain dedicated for all real-world assets (RWAs) that integrates asset tokenization and compliance providers directly into the chain. Our mission is to simplify the convoluted processes of RWA project deployment and offer investors a blockchain ecosystem to cross-pollinate and invest in various RWAs. In addition, Plume enables RWA composability through its thriving DeFi applications and provides access to high-quality buyers to increase liquidity for all tokenized RWAs.",
|
||||
cta: {
|
||||
backgroundImageUrl: plumeCTA.src,
|
||||
title: "Participate on the Testnet Now!",
|
||||
buttonLink: "https://miles.plumenetwork.xyz/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
//zkCandy
|
||||
302: {
|
||||
headerImgUrl: thirdwebBanner.src,
|
||||
about:
|
||||
"Explore our cutting-edge Layer2 Ethereum scaling ZK Chain built for gaming and entertainment with the latest zk-proof technology.",
|
||||
cta: {
|
||||
backgroundImageUrl: thirdwebCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://zkcandy.io/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
//zkSync
|
||||
324: {
|
||||
headerImgUrl: thirdwebBanner.src,
|
||||
about:
|
||||
"ZKsync is an ever expanding verifiable blockchain network, secured by math.",
|
||||
cta: {
|
||||
backgroundImageUrl: thirdwebCTA.src,
|
||||
title: "",
|
||||
buttonLink: "https://zksync.io/",
|
||||
buttonText: "Learn more",
|
||||
},
|
||||
},
|
||||
} satisfies Record<number, ChainMetadata>;
|
||||
// END TEMPORARY
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { THIRDWEB_API_HOST } from "../../../../../../../../constants/urls";
|
||||
import { FetchError } from "../../../../../../../../utils/error";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { FetchError } from "utils/error";
|
||||
import type { Ecosystem } from "../../../types";
|
||||
|
||||
export function useEcosystem({
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
useMutation,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import { upload } from "thirdweb/storage";
|
||||
import { THIRDWEB_API_HOST } from "../../../../../../../constants/urls";
|
||||
|
||||
export type CreateEcosystemParams = {
|
||||
name: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { THIRDWEB_API_HOST } from "../../../../../../constants/urls";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import type { Ecosystem } from "../types";
|
||||
|
||||
export function useEcosystemList() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AppFooter } from "@/components/blocks/app-footer";
|
||||
import { DashboardHeader } from "../components/Header/DashboardHeader";
|
||||
import { DashboardHeader } from "app/components/Header/DashboardHeader";
|
||||
|
||||
export default function DashboardLayout(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Separator } from "../../../@/components/ui/separator";
|
||||
import { cn } from "../../../@/lib/utils";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const metadata = {
|
||||
robots: "noindex, nofollow",
|
||||
|
||||
|
After Width: | Height: | Size: 849 KiB |
@@ -7,6 +7,7 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { BookOpenIcon, ChevronRightIcon } from "lucide-react";
|
||||
import type { Metadata } from "next";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { SiDiscord } from "react-icons/si";
|
||||
@@ -18,6 +19,12 @@ import miscIcon from "../../../../public/assets/support/misc.svg";
|
||||
import connectIcon from "../../../../public/assets/support/wallets.png";
|
||||
import { CreateTicket } from "./components/create-ticket.client";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "thirdweb Support",
|
||||
openGraph: {
|
||||
title: "thirdweb Support",
|
||||
},
|
||||
};
|
||||
const HELP_PRODUCTS = [
|
||||
{
|
||||
title: "Connect",
|
||||
|
||||
@@ -8,9 +8,7 @@ import {
|
||||
} from "@/components/ui/pagination";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
import React from "react";
|
||||
|
||||
export function TablePagination(props: { isLandingPage?: boolean }) {
|
||||
export function TablePagination() {
|
||||
const router = useRouter();
|
||||
const path = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
@@ -22,7 +20,6 @@ export function TablePagination(props: { isLandingPage?: boolean }) {
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
className={`${props.isLandingPage && "bg-black/50"}`}
|
||||
disabled={page <= 1}
|
||||
onClick={() => {
|
||||
if (page === 1) return;
|
||||
@@ -35,10 +32,11 @@ export function TablePagination(props: { isLandingPage?: boolean }) {
|
||||
}}
|
||||
/>
|
||||
</PaginationItem>
|
||||
<PaginationItem className="px-4">{page}</PaginationItem>
|
||||
<PaginationItem className="px-4 border rounded-lg h-full flex items-center bg-secondary">
|
||||
{page}
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
className={`${props.isLandingPage && "bg-black/50"}`}
|
||||
onClick={() => {
|
||||
router.replace(
|
||||
`${path}?page=${Number(page) + 1}${range?.length ? `&timeRange=${range}` : ""}${currentSort ? `&sortBy=${currentSort}` : ""}`,
|
||||
|
||||