Compare commits
230
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eb5d8c01e | ||
|
|
d036b87ae1 | ||
|
|
c86cac4d61 | ||
|
|
af09576247 | ||
|
|
316cef1a58 | ||
|
|
1bfcd375e4 | ||
|
|
86070fa309 | ||
|
|
c5bf5a8594 | ||
|
|
c1d6bb7848 | ||
|
|
e9db59e8da | ||
|
|
1db216545a | ||
|
|
0a798c58b3 | ||
|
|
cfacfc28a8 | ||
|
|
2d978aea35 | ||
|
|
ca382f442a | ||
|
|
840b1a264f | ||
|
|
d803d37879 | ||
|
|
e10c1e696e | ||
|
|
a2305329fd | ||
|
|
60235ade34 | ||
|
|
811384b91f | ||
|
|
22dc4d7cd1 | ||
|
|
3074e7c67d | ||
|
|
dd99410ccd | ||
|
|
b4e5dc1f3c | ||
|
|
8b4091e848 | ||
|
|
2d15e6f509 | ||
|
|
e450d320a0 | ||
|
|
838f8fa547 | ||
|
|
1dae74b9d2 | ||
|
|
672f7d00bd | ||
|
|
a9eda9fc8d | ||
|
|
a3b4720f69 | ||
|
|
c2a5f65239 | ||
|
|
7ab76ac55a | ||
|
|
386138e4f0 | ||
|
|
309d1d544b | ||
|
|
59be4ea2fb | ||
|
|
9c737a1905 | ||
|
|
646a5a3788 | ||
|
|
7597418872 | ||
|
|
5204bd506f | ||
|
|
663627715a | ||
|
|
8f7bcaeadc | ||
|
|
ad7f78ca0b | ||
|
|
0967a5d53a | ||
|
|
d93765ac54 | ||
|
|
169ae22a86 | ||
|
|
04d2bc72e8 | ||
|
|
1a236fd103 | ||
|
|
4691343c7d | ||
|
|
93f225b8a3 | ||
|
|
e9649a2b53 | ||
|
|
4d39fb6ee3 | ||
|
|
1e092a7bff | ||
|
|
80ee3d6670 | ||
|
|
a7fd4e51a3 | ||
|
|
61583cf6f3 | ||
|
|
cf02af06a4 | ||
|
|
6001f2487e | ||
|
|
3f21445369 | ||
|
|
9e8fd89ec0 | ||
|
|
95a87530e3 | ||
|
|
5faaf1be48 | ||
|
|
4847ada819 | ||
|
|
08eea95f9b | ||
|
|
737155ba21 | ||
|
|
73ae5817f2 | ||
|
|
6a0704a19b | ||
|
|
2ef8f2a371 | ||
|
|
d6373b2c67 | ||
|
|
7eaf86a607 | ||
|
|
5788cbdbad | ||
|
|
d9a39cde33 | ||
|
|
62c8e7c0c4 | ||
|
|
f618c5308c | ||
|
|
04943d730b | ||
|
|
2606da6d87 | ||
|
|
9cf137660a | ||
|
|
58ab8c2ee7 | ||
|
|
447a519ddc | ||
|
|
a9b7633d05 | ||
|
|
38e089a5f3 | ||
|
|
ef2f86cff0 | ||
|
|
8fdef82147 | ||
|
|
cbb15692a7 | ||
|
|
1d23e5cd9c | ||
|
|
a17d893811 | ||
|
|
0dcde077f5 | ||
|
|
05a041fac3 | ||
|
|
3d140eed97 | ||
|
|
a5f005fff0 | ||
|
|
fb70cab6d4 | ||
|
|
cd9ea69c13 | ||
|
|
dc892bd136 | ||
|
|
3d7c009d72 | ||
|
|
6dbed0d4fc | ||
|
|
adee1903d5 | ||
|
|
a1d785b8ed | ||
|
|
ba92cd2cbe | ||
|
|
c3d0ea0059 | ||
|
|
35c5f57179 | ||
|
|
2a1f4be48b | ||
|
|
0121ccd702 | ||
|
|
12bcfc596a | ||
|
|
a4ff738268 | ||
|
|
b49c191355 | ||
|
|
06f0765665 | ||
|
|
12adbd8314 | ||
|
|
48e014d426 | ||
|
|
826748bb5d | ||
|
|
d4f5a9db12 | ||
|
|
3b827ea32c | ||
|
|
2231db7980 | ||
|
|
f9872f442c | ||
|
|
473a25237e | ||
|
|
2d4c5ff07c | ||
|
|
ab738eb7ba | ||
|
|
8e6651e591 | ||
|
|
e5026a10ab | ||
|
|
9c1c5f2fc8 | ||
|
|
45cdb86911 | ||
|
|
fe3351ce79 | ||
|
|
fc657e0b89 | ||
|
|
cff2481c06 | ||
|
|
e0e31826a8 | ||
|
|
fe8b3ce5a5 | ||
|
|
3c120a7584 | ||
|
|
b51157c0ff | ||
|
|
f1df19e612 | ||
|
|
98781531bb | ||
|
|
d3a5ea7833 | ||
|
|
1a9448e152 | ||
|
|
2fe16119f5 | ||
|
|
c549d5934e | ||
|
|
dd65506dfb | ||
|
|
26cfd1b30e | ||
|
|
495d8edfa2 | ||
|
|
88c213b6d5 | ||
|
|
d9091bd2e7 | ||
|
|
ab7bf1ad1d | ||
|
|
21389bbef6 | ||
|
|
4f51201249 | ||
|
|
a0cee488e9 | ||
|
|
0b8907cb7c | ||
|
|
85a78d4460 | ||
|
|
2a6778eb42 | ||
|
|
882fd3dcbd | ||
|
|
744491f853 | ||
|
|
d3f788471b | ||
|
|
6db81ef68d | ||
|
|
f8b2bdc64b | ||
|
|
c407bd10c7 | ||
|
|
e540dc11f5 | ||
|
|
7476b7927d | ||
|
|
ccb641698e | ||
|
|
aa9d858f1c | ||
|
|
d8540216f1 | ||
|
|
654f8795b4 | ||
|
|
279fb918f5 | ||
|
|
c5cff26faa | ||
|
|
78901457cf | ||
|
|
47f8cd6d8d | ||
|
|
d14a78c06c | ||
|
|
f81fd46e9c | ||
|
|
d6ce3aedd8 | ||
|
|
6672549edd | ||
|
|
639b542d27 | ||
|
|
b5fb5dd0cc | ||
|
|
8b036dcfa7 | ||
|
|
56bc4ae335 | ||
|
|
71f83a884b | ||
|
|
2d5ff74335 | ||
|
|
cbc3bf1d36 | ||
|
|
20ead112e2 | ||
|
|
06d25bb7a9 | ||
|
|
91f8b2dbf2 | ||
|
|
9901351fb2 | ||
|
|
cb86fa2929 | ||
|
|
53718208cd | ||
|
|
ef2613aca5 | ||
|
|
f2e61b83cb | ||
|
|
ab0d33016d | ||
|
|
f54a0cb39b | ||
|
|
90010ee723 | ||
|
|
269fbe5ecf | ||
|
|
7ed8fc2c43 | ||
|
|
9f2f2e794a | ||
|
|
eefd2649fd | ||
|
|
62de6dd04c | ||
|
|
6fc6c6e681 | ||
|
|
58cd79426d | ||
|
|
185e8f48df | ||
|
|
d8865b74ef | ||
|
|
1e8d1e4b52 | ||
|
|
91194ce63a | ||
|
|
a1141f316f | ||
|
|
1273cc7b83 | ||
|
|
691110286d | ||
|
|
4d197a822d | ||
|
|
f655657fce | ||
|
|
109f250068 | ||
|
|
f7cf09b3cb | ||
|
|
dd06be8def | ||
|
|
bf8da3588f | ||
|
|
f6cbdb7e30 | ||
|
|
d350f8cb13 | ||
|
|
4b200503b8 | ||
|
|
f8eb86da47 | ||
|
|
2ae778ed7f | ||
|
|
1c135b0f65 | ||
|
|
286114ab81 | ||
|
|
cbc4e80330 | ||
|
|
efceadd3c1 | ||
|
|
c653d7033d | ||
|
|
77029a2e01 | ||
|
|
b0b3b7f8f6 | ||
|
|
617fe68c32 | ||
|
|
01d0666208 | ||
|
|
621357a5c6 | ||
|
|
0527ee26b7 | ||
|
|
834ed8337b | ||
|
|
11bb4f5fee | ||
|
|
c547164320 | ||
|
|
38ae2d3c6b | ||
|
|
d75320191d | ||
|
|
6046d4510d | ||
|
|
f1d32261f0 | ||
|
|
04baa74ad7 | ||
|
|
44bb0034a6 |
@@ -6,15 +6,15 @@ runs:
|
||||
steps:
|
||||
# we use bun for some test suites
|
||||
- name: Setup bun
|
||||
uses: oven-sh/setup-bun@v1
|
||||
uses: oven-sh/setup-bun@4bc047ad259df6fc24a6c9b0f9a0cb08cf17fbe5 # v2.0.1
|
||||
with:
|
||||
bun-version: 1.0.35
|
||||
# pnpm for our dependencies
|
||||
- uses: pnpm/action-setup@v3
|
||||
- uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4.1.0
|
||||
with:
|
||||
version: 9
|
||||
version: 9.11.0
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
|
||||
+10
-10
@@ -26,7 +26,7 @@ jobs:
|
||||
steps:
|
||||
- name: Optimize CI
|
||||
id: check_skip
|
||||
uses: withgraphite/graphite-ci-action@main
|
||||
uses: withgraphite/graphite-ci-action@9cb601a55e114099561b6d755505de377d45db40 # v0.0.9 ("main")
|
||||
with:
|
||||
graphite_token: ${{ secrets.GRAPHITE_OMTIMIZE_TOKEN }}
|
||||
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
name: Build Packages
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup & Install
|
||||
uses: ./.github/composite-actions/install
|
||||
@@ -54,13 +54,13 @@ jobs:
|
||||
runs-on: ubuntu-latest-8
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup & Install
|
||||
uses: ./.github/composite-actions/install
|
||||
|
||||
- name: Setup Biome
|
||||
uses: biomejs/setup-biome@v2
|
||||
uses: biomejs/setup-biome@f382a98e582959e6aaac8e5f8b17b31749018780 # v2.5.0
|
||||
with:
|
||||
version: latest
|
||||
|
||||
@@ -74,13 +74,13 @@ jobs:
|
||||
runs-on: ubuntu-latest-8
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup & Install
|
||||
uses: ./.github/composite-actions/install
|
||||
|
||||
- name: Set up foundry
|
||||
uses: foundry-rs/foundry-toolchain@v1
|
||||
uses: foundry-rs/foundry-toolchain@de808b1eea699e761c404bda44ba8f21aba30b2c # v1.3.1
|
||||
with:
|
||||
cache: false
|
||||
version: nightly-c4a984fbf2c48b793c8cd53af84f56009dd1070c
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
- run: pnpm test
|
||||
|
||||
- name: Code Coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@0565863a31f2c772f9f0395002a31e3f06189574 # v5.4.0
|
||||
with:
|
||||
directory: packages/
|
||||
flags: packages
|
||||
@@ -106,7 +106,7 @@ jobs:
|
||||
bundler: [vite, webpack, esbuild]
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup & Install
|
||||
uses: ./.github/composite-actions/install
|
||||
@@ -172,13 +172,13 @@ jobs:
|
||||
runs-on: ubuntu-latest-8
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Setup & Install
|
||||
uses: ./.github/composite-actions/install
|
||||
|
||||
- name: Report bundle size
|
||||
uses: andresz1/size-limit-action@master
|
||||
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
package_manager: pnpm
|
||||
|
||||
@@ -16,4 +16,4 @@ jobs:
|
||||
github.event.pull_request.author_association == 'COLLABORATOR' ||
|
||||
github.event.pull_request.author_association == 'CONTRIBUTOR'
|
||||
steps:
|
||||
- uses: toshimaru/[email protected]
|
||||
- uses: toshimaru/auto-author-assign@16f0022cf3d7970c106d8d1105f75a1165edb516 # v2.1.1
|
||||
|
||||
@@ -42,11 +42,11 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
uses: github/codeql-action/init@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
@@ -59,7 +59,7 @@ jobs:
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v3
|
||||
uses: github/codeql-action/autobuild@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
@@ -72,4 +72,4 @@ jobs:
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
uses: github/codeql-action/analyze@6bb031afdd8eb862ea3fc1848194185e076637e5 # v3.28.11
|
||||
|
||||
@@ -17,6 +17,6 @@ jobs:
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Do not use the GITHUB_TOKEN by default
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout branch
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
# Do not use the GITHUB_TOKEN by default
|
||||
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
- name: Create release Pull Request or publish to NPM
|
||||
id: changesets
|
||||
uses: changesets/action@v1
|
||||
uses: changesets/action@06245a4e0a36c064a573d4150030f5ec548e4fcc #v1.4.0
|
||||
with:
|
||||
publish: pnpm release
|
||||
version: pnpm version-packages
|
||||
|
||||
@@ -7,7 +7,7 @@ jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v.9.1.0
|
||||
with:
|
||||
stale-issue-message: 'This issue has been inactive for 7 days. It is now marked as stale and will be closed in 2 days if no further activity occurs.'
|
||||
stale-pr-message: 'This PR has been inactive for 7 days. It is now marked as stale and will be closed in 2 days if no further activity occurs.'
|
||||
|
||||
@@ -21,30 +21,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install
|
||||
uses: ./.github/composite-actions/install
|
||||
|
||||
- name: Run TypeDoc
|
||||
run: pnpm typedoc
|
||||
|
||||
- name: Update Gist
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GIST_TOKEN }}
|
||||
script: |
|
||||
const fs = require('fs');
|
||||
const content = fs.readFileSync('./packages/thirdweb/typedoc/parsed.json', 'utf8');
|
||||
const gistId = '678fe1f331a01270bb002fee660f285d';
|
||||
|
||||
await github.rest.gists.update({
|
||||
gist_id: gistId,
|
||||
files: {
|
||||
'data.json': {
|
||||
content: content
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`Permalink: https://gist.githubusercontent.com/raw/${gistId}/data.json`);
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
public-hoist-pattern[]=*import-in-the-middle*
|
||||
public-hoist-pattern[]=*require-in-the-middle*
|
||||
@@ -102,3 +102,6 @@ ANALYTICS_SERVICE_URL=""
|
||||
|
||||
# Required for Nebula Chat
|
||||
NEXT_PUBLIC_NEBULA_URL=""
|
||||
|
||||
# required for billing parts of the dashboard (team -> settings -> billing / invoices)
|
||||
STRIPE_SECRET_KEY=""
|
||||
@@ -27,5 +27,8 @@ const config: StorybookConfig = {
|
||||
},
|
||||
},
|
||||
staticDirs: ["../public"],
|
||||
features: {
|
||||
experimentalRSC: true,
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Preview } from "@storybook/react";
|
||||
import "@/styles/globals.css";
|
||||
import "../src/global.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
@@ -7,6 +7,7 @@ import { Inter as interFont } from "next/font/google";
|
||||
// biome-ignore lint/style/useImportType: <explanation>
|
||||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { Button } from "../src/@/components/ui/button";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
@@ -16,8 +17,30 @@ const fontSans = interFont({
|
||||
variable: "--font-sans",
|
||||
});
|
||||
|
||||
const customViewports = {
|
||||
xs: {
|
||||
// Regular sized phones (iphone 15 / 15 pro)
|
||||
name: "iPhone",
|
||||
styles: {
|
||||
width: "390px",
|
||||
height: "844px",
|
||||
},
|
||||
},
|
||||
sm: {
|
||||
// Larger phones (iphone 15 plus / 15 pro max)
|
||||
name: "iPhone Plus",
|
||||
styles: {
|
||||
width: "430px",
|
||||
height: "932px",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const preview: Preview = {
|
||||
parameters: {
|
||||
viewport: {
|
||||
viewports: customViewports,
|
||||
},
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
@@ -57,29 +80,30 @@ function StoryLayout(props: {
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="flex min-h-screen min-w-0 flex-col bg-background text-foreground">
|
||||
<div className="flex justify-end gap-2 border-b p-4">
|
||||
<div className="flex min-h-dvh min-w-0 flex-col bg-background text-foreground">
|
||||
<div className="fixed right-0 bottom-0 z-50 flex justify-end gap-2 p-4">
|
||||
<Button
|
||||
onClick={() => setTheme("dark")}
|
||||
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
|
||||
size="sm"
|
||||
variant={theme === "dark" ? "default" : "outline"}
|
||||
className="h-auto w-auto rounded-full p-2"
|
||||
variant="outline"
|
||||
className="h-auto w-auto shrink-0 rounded-full p-2"
|
||||
>
|
||||
<MoonIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setTheme("light")}
|
||||
size="sm"
|
||||
variant={theme === "light" ? "default" : "outline"}
|
||||
className="h-auto w-auto rounded-full p-2"
|
||||
>
|
||||
<SunIcon className="size-4" />
|
||||
{theme === "dark" ? (
|
||||
<MoonIcon className="size-4" />
|
||||
) : (
|
||||
<SunIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 grow flex-col">{props.children}</div>
|
||||
<ToasterSetup />
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function ToasterSetup() {
|
||||
const { theme } = useTheme();
|
||||
return <Toaster richColors theme={theme === "light" ? "light" : "dark"} />;
|
||||
}
|
||||
|
||||
@@ -49,12 +49,13 @@ module.exports = [
|
||||
"/templates/:template_slug",
|
||||
// -- learn --
|
||||
"/learn",
|
||||
"/learn/tutorials",
|
||||
"/learn/tutorials/:tutorial_slug",
|
||||
"/learn/guides",
|
||||
"/learn/guides/:guide_slug",
|
||||
"/learn/courses",
|
||||
"/learn/glossary",
|
||||
"/learn/glossary/:glossary_slug",
|
||||
// -- faucets --
|
||||
"/faucets",
|
||||
// -- brand kit --
|
||||
"/brand-kit",
|
||||
];
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"ignoreDependencies": [
|
||||
"@storybook/blocks",
|
||||
"@thirdweb-dev/service-utils",
|
||||
"@types/color"
|
||||
"@types/color",
|
||||
"fast-xml-parser"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// @ts-check
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { XMLParser } = require("fast-xml-parser");
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<Array<{chainId: number, name: string, slug: string}>>}
|
||||
*/
|
||||
async function fetchChainsFromApi() {
|
||||
const res = await fetch("https://api.thirdweb.com/v1/chains", {
|
||||
@@ -82,16 +84,20 @@ module.exports = {
|
||||
};
|
||||
},
|
||||
additionalPaths: async (config) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const FRAMER_PATHS = require("./framer-rewrites");
|
||||
const allChains = await fetchChainsFromApi();
|
||||
const [framerUrls, allChains] = await Promise.all([
|
||||
getFramerXML(),
|
||||
fetchChainsFromApi(),
|
||||
]);
|
||||
|
||||
return [
|
||||
...FRAMER_PATHS.map((path) => ({
|
||||
loc: path,
|
||||
changefreq: config.changefreq,
|
||||
priority: config.priority,
|
||||
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
|
||||
})),
|
||||
...framerUrls.map((url) => {
|
||||
return {
|
||||
loc: url.loc,
|
||||
changefreq: config.changefreq,
|
||||
priority: config.priority,
|
||||
lastmod: config.autoLastmod ? new Date().toISOString() : undefined,
|
||||
};
|
||||
}),
|
||||
...allChains.map((chain) => {
|
||||
return {
|
||||
loc: `/${chain.slug}`,
|
||||
@@ -136,3 +142,19 @@ async function createSearchRecordSitemaps(config) {
|
||||
// filter out any failed requests
|
||||
return chainsForLines.filter(Boolean);
|
||||
}
|
||||
|
||||
async function getFramerXML() {
|
||||
const framerSiteMapText = await fetch(
|
||||
"https://landing.thirdweb.com/sitemap.xml",
|
||||
).then((res) => res.text());
|
||||
|
||||
const parser = new XMLParser();
|
||||
const xmlObject = parser.parse(framerSiteMapText);
|
||||
|
||||
/**
|
||||
* @type {Array<{loc: string}>}
|
||||
*/
|
||||
const urls = xmlObject.urlset.url;
|
||||
|
||||
return urls;
|
||||
}
|
||||
|
||||
@@ -203,7 +203,6 @@ function getConfig(): NextConfig {
|
||||
type: "memory",
|
||||
});
|
||||
}
|
||||
config.externals.push("pino-pretty");
|
||||
config.module = {
|
||||
...config.module,
|
||||
exprContextCritical: false,
|
||||
|
||||
+58
-55
@@ -29,29 +29,29 @@
|
||||
"@emotion/styled": "11.14.0",
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@marsidev/react-turnstile": "^1.1.0",
|
||||
"@radix-ui/react-accordion": "^1.2.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.6",
|
||||
"@radix-ui/react-avatar": "^1.1.3",
|
||||
"@radix-ui/react-checkbox": "^1.1.4",
|
||||
"@radix-ui/react-dialog": "1.1.6",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||
"@radix-ui/react-hover-card": "^1.1.6",
|
||||
"@radix-ui/react-label": "^2.1.2",
|
||||
"@radix-ui/react-popover": "^1.1.6",
|
||||
"@radix-ui/react-progress": "^1.1.2",
|
||||
"@radix-ui/react-radio-group": "^1.2.3",
|
||||
"@radix-ui/react-select": "^2.1.6",
|
||||
"@radix-ui/react-separator": "^1.1.2",
|
||||
"@radix-ui/react-slot": "^1.1.2",
|
||||
"@radix-ui/react-switch": "^1.1.3",
|
||||
"@radix-ui/react-tooltip": "1.1.8",
|
||||
"@sentry/nextjs": "9.3.0",
|
||||
"@shazow/whatsabi": "0.20.0",
|
||||
"@tanstack/react-query": "5.67.1",
|
||||
"@radix-ui/react-accordion": "^1.2.4",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.7",
|
||||
"@radix-ui/react-avatar": "^1.1.4",
|
||||
"@radix-ui/react-checkbox": "^1.1.5",
|
||||
"@radix-ui/react-dialog": "1.1.7",
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.7",
|
||||
"@radix-ui/react-hover-card": "^1.1.7",
|
||||
"@radix-ui/react-label": "^2.1.3",
|
||||
"@radix-ui/react-popover": "^1.1.7",
|
||||
"@radix-ui/react-progress": "^1.1.3",
|
||||
"@radix-ui/react-radio-group": "^1.2.4",
|
||||
"@radix-ui/react-select": "^2.1.7",
|
||||
"@radix-ui/react-separator": "^1.1.3",
|
||||
"@radix-ui/react-slot": "^1.2.0",
|
||||
"@radix-ui/react-switch": "^1.1.4",
|
||||
"@radix-ui/react-tooltip": "1.2.0",
|
||||
"@sentry/nextjs": "9.12.0",
|
||||
"@shazow/whatsabi": "0.21.0",
|
||||
"@tanstack/react-query": "5.72.1",
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "2.0.0",
|
||||
"@vercel/og": "^0.6.5",
|
||||
"@vercel/og": "^0.6.8",
|
||||
"abitype": "1.0.8",
|
||||
"chakra-react-select": "^4.7.6",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
@@ -59,42 +59,45 @@
|
||||
"color": "5.0.0",
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"fast-xml-parser": "^5.2.0",
|
||||
"fetch-event-stream": "0.1.5",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "12.4.10",
|
||||
"framer-motion": "12.5.0",
|
||||
"fuse.js": "7.1.0",
|
||||
"idb-keyval": "^6.2.1",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.5.0",
|
||||
"ioredis": "^5.6.0",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.477.0",
|
||||
"next": "15.2.0",
|
||||
"lucide-react": "0.487.0",
|
||||
"next": "15.2.4",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.4",
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.0",
|
||||
"nuqs": "^2.4.1",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.5.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.67.1",
|
||||
"prettier": "3.5.3",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "19.0.0",
|
||||
"react": "19.1.0",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
"react-day-picker": "^8.10.1",
|
||||
"react-dom": "19.0.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-error-boundary": "^5.0.0",
|
||||
"react-hook-form": "7.54.2",
|
||||
"react-hook-form": "7.55.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "2.15.1",
|
||||
"recharts": "2.15.2",
|
||||
"remark-gfm": "4.0.1",
|
||||
"responsive-rsc": "0.0.7",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"sonner": "2.0.1",
|
||||
"sonner": "2.0.3",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"swagger-ui-react": "^5.20.0",
|
||||
"stripe": "17.7.0",
|
||||
"swagger-ui-react": "^5.20.7",
|
||||
"tailwind-merge": "^2.6.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"thirdweb": "workspace:*",
|
||||
@@ -104,42 +107,42 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.5",
|
||||
"@next/bundle-analyzer": "15.2.0",
|
||||
"@next/eslint-plugin-next": "15.2.0",
|
||||
"@playwright/test": "1.50.1",
|
||||
"@storybook/addon-essentials": "8.6.3",
|
||||
"@storybook/addon-interactions": "8.6.3",
|
||||
"@storybook/addon-links": "8.6.3",
|
||||
"@storybook/addon-onboarding": "8.6.3",
|
||||
"@storybook/addon-viewport": "8.6.3",
|
||||
"@storybook/blocks": "8.6.3",
|
||||
"@storybook/nextjs": "8.6.3",
|
||||
"@storybook/react": "8.6.3",
|
||||
"@storybook/test": "8.6.3",
|
||||
"@chromatic-com/storybook": "3.2.6",
|
||||
"@next/bundle-analyzer": "15.2.4",
|
||||
"@next/eslint-plugin-next": "15.2.4",
|
||||
"@playwright/test": "1.51.1",
|
||||
"@storybook/addon-essentials": "8.6.12",
|
||||
"@storybook/addon-interactions": "8.6.12",
|
||||
"@storybook/addon-links": "8.6.12",
|
||||
"@storybook/addon-onboarding": "8.6.12",
|
||||
"@storybook/addon-viewport": "8.6.12",
|
||||
"@storybook/blocks": "8.6.12",
|
||||
"@storybook/nextjs": "8.6.12",
|
||||
"@storybook/react": "8.6.12",
|
||||
"@storybook/test": "8.6.12",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "22.13.9",
|
||||
"@types/node": "22.14.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "19.0.10",
|
||||
"@types/react-dom": "19.0.4",
|
||||
"@types/react": "19.1.0",
|
||||
"@types/react-dom": "19.1.1",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
"@types/swagger-ui-react": "^4.19.0",
|
||||
"@typescript-eslint/eslint-plugin": "7.14.1",
|
||||
"@typescript-eslint/parser": "7.14.1",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"checkly": "5.0.1",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"checkly": "5.2.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.4",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e1e972c-20250221",
|
||||
"eslint-plugin-storybook": "0.11.3",
|
||||
"knip": "5.45.0",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250405",
|
||||
"eslint-plugin-storybook": "0.12.0",
|
||||
"knip": "5.47.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.5.3",
|
||||
"storybook": "8.6.3",
|
||||
"storybook": "8.6.12",
|
||||
"tailwindcss": "3.4.17",
|
||||
"typescript": "5.8.2"
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -354,6 +354,26 @@ async function redirects() {
|
||||
destination: "/",
|
||||
permanent: false,
|
||||
},
|
||||
// pay > universal-bridge redirect
|
||||
{
|
||||
source: "/team/:team_slug/:project_slug/connect/pay/:path*",
|
||||
destination:
|
||||
"/team/:team_slug/:project_slug/connect/universal-bridge/:path*",
|
||||
permanent: false,
|
||||
},
|
||||
|
||||
// all /learn/tutorials (and sub-routes) -> /learn/guides
|
||||
{
|
||||
source: "/learn/tutorials/:path*",
|
||||
destination: "/learn/guides/:path*",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/learn/tutorials",
|
||||
destination: "/learn/guides",
|
||||
permanent: false,
|
||||
},
|
||||
|
||||
...legacyDashboardToTeamRedirects,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
"use server";
|
||||
|
||||
import "server-only";
|
||||
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import type { ProductSKU } from "../lib/billing";
|
||||
|
||||
export type RedirectCheckoutOptions = {
|
||||
export type GetBillingCheckoutUrlOptions = {
|
||||
teamSlug: string;
|
||||
sku: ProductSKU;
|
||||
redirectUrl: string;
|
||||
metadata?: Record<string, string>;
|
||||
};
|
||||
|
||||
export async function redirectToCheckout(
|
||||
options: RedirectCheckoutOptions,
|
||||
): Promise<{ status: number }> {
|
||||
export async function getBillingCheckoutUrl(
|
||||
options: GetBillingCheckoutUrlOptions,
|
||||
): Promise<{ status: number; url?: string }> {
|
||||
if (!options.teamSlug) {
|
||||
return {
|
||||
status: 400,
|
||||
@@ -49,6 +48,7 @@ export async function redirectToCheckout(
|
||||
status: res.status,
|
||||
};
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
if (!json.result) {
|
||||
return {
|
||||
@@ -56,20 +56,22 @@ export async function redirectToCheckout(
|
||||
};
|
||||
}
|
||||
|
||||
// redirect to the stripe checkout session
|
||||
redirect(json.result);
|
||||
return {
|
||||
status: 200,
|
||||
url: json.result as string,
|
||||
};
|
||||
}
|
||||
|
||||
export type RedirectBillingCheckoutAction = typeof redirectToCheckout;
|
||||
export type GetBillingCheckoutUrlAction = typeof getBillingCheckoutUrl;
|
||||
|
||||
export type BillingPortalOptions = {
|
||||
export type GetBillingPortalUrlOptions = {
|
||||
teamSlug: string | undefined;
|
||||
redirectUrl: string;
|
||||
};
|
||||
|
||||
export async function redirectToBillingPortal(
|
||||
options: BillingPortalOptions,
|
||||
): Promise<{ status: number }> {
|
||||
export async function getBillingPortalUrl(
|
||||
options: GetBillingPortalUrlOptions,
|
||||
): Promise<{ status: number; url?: string }> {
|
||||
if (!options.teamSlug) {
|
||||
return {
|
||||
status: 400,
|
||||
@@ -110,8 +112,10 @@ export async function redirectToBillingPortal(
|
||||
};
|
||||
}
|
||||
|
||||
// redirect to the stripe billing portal
|
||||
redirect(json.result);
|
||||
return {
|
||||
status: 200,
|
||||
url: json.result as string,
|
||||
};
|
||||
}
|
||||
|
||||
export type BillingBillingPortalAction = typeof redirectToBillingPortal;
|
||||
export type GetBillingPortalUrlAction = typeof getBillingPortalUrl;
|
||||
|
||||
@@ -10,12 +10,9 @@ import {
|
||||
isMoralisSupported,
|
||||
transformMoralisResponseToNFT,
|
||||
} from "lib/wallet/nfts/moralis";
|
||||
import {
|
||||
generateSimpleHashUrl,
|
||||
isSimpleHashSupported,
|
||||
transformSimpleHashResponseToNFT,
|
||||
} from "lib/wallet/nfts/simpleHash";
|
||||
import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
import { getVercelEnv } from "../../lib/vercel-utils";
|
||||
import { DASHBOARD_THIRDWEB_CLIENT_ID } from "../constants/env";
|
||||
|
||||
type WalletNFTApiReturn =
|
||||
| { result: WalletNFT[]; error?: undefined }
|
||||
@@ -24,40 +21,20 @@ type WalletNFTApiReturn =
|
||||
export async function getWalletNFTs(params: {
|
||||
chainId: number;
|
||||
owner: string;
|
||||
isInsightSupported: boolean;
|
||||
}): Promise<WalletNFTApiReturn> {
|
||||
const { chainId, owner } = params;
|
||||
const supportedChainSlug = await isSimpleHashSupported(chainId);
|
||||
|
||||
if (supportedChainSlug && process.env.SIMPLEHASH_API_KEY) {
|
||||
const url = generateSimpleHashUrl({ chainSlug: supportedChainSlug, owner });
|
||||
if (params.isInsightSupported) {
|
||||
const response = await getWalletNFTsFromInsight({ chainId, owner });
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-API-KEY": process.env.SIMPLEHASH_API_KEY,
|
||||
},
|
||||
next: {
|
||||
revalidate: 10, // cache for 10 seconds
|
||||
},
|
||||
});
|
||||
|
||||
if (response.status >= 400) {
|
||||
if (!response.ok) {
|
||||
return {
|
||||
error: response.statusText,
|
||||
error: response.error,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedResponse = await response.json();
|
||||
const result = await transformSimpleHashResponseToNFT(
|
||||
parsedResponse,
|
||||
owner,
|
||||
);
|
||||
|
||||
return { result };
|
||||
} catch {
|
||||
return { error: "error parsing response" };
|
||||
}
|
||||
return { result: response.data };
|
||||
}
|
||||
|
||||
if (isAlchemySupported(chainId)) {
|
||||
@@ -115,3 +92,115 @@ export async function getWalletNFTs(params: {
|
||||
|
||||
return { error: "unsupported chain" };
|
||||
}
|
||||
|
||||
type OwnedNFTInsightResponse = {
|
||||
name: string;
|
||||
description: string;
|
||||
image_url: string;
|
||||
background_color: string;
|
||||
external_url: string;
|
||||
metadata_url: string;
|
||||
extra_metadata: {
|
||||
customImage?: string;
|
||||
customAnimationUrl?: string;
|
||||
animation_original_url?: string;
|
||||
image_original_url?: string;
|
||||
};
|
||||
collection: {
|
||||
name: string;
|
||||
description: string;
|
||||
extra_metadata: Record<string, string>;
|
||||
};
|
||||
contract: {
|
||||
chain_id: number;
|
||||
address: string;
|
||||
type: "erc1155" | "erc721";
|
||||
name: string;
|
||||
};
|
||||
owner_addresses: string[];
|
||||
token_id: string;
|
||||
balance: string;
|
||||
token_type: "erc1155" | "erc721";
|
||||
};
|
||||
|
||||
async function getWalletNFTsFromInsight(params: {
|
||||
chainId: number;
|
||||
owner: string;
|
||||
}): Promise<
|
||||
| {
|
||||
data: WalletNFT[];
|
||||
ok: true;
|
||||
}
|
||||
| {
|
||||
ok: false;
|
||||
error: string;
|
||||
}
|
||||
> {
|
||||
const { chainId, owner } = params;
|
||||
|
||||
const thirdwebDomain =
|
||||
getVercelEnv() === "production" ? "thirdweb" : "thirdweb-dev";
|
||||
const url = new URL(`https://insight.${thirdwebDomain}.com/v1/nfts`);
|
||||
url.searchParams.append("chain", chainId.toString());
|
||||
url.searchParams.append("limit", "10");
|
||||
url.searchParams.append("owner_address", owner);
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
"x-client-id": DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorMessage = await response.text();
|
||||
return {
|
||||
ok: false,
|
||||
error: errorMessage,
|
||||
};
|
||||
}
|
||||
|
||||
const nftsResponse = (await response.json()) as {
|
||||
data: OwnedNFTInsightResponse[];
|
||||
};
|
||||
|
||||
const isDev = getVercelEnv() !== "production";
|
||||
|
||||
// NOTE: ipfscdn.io/ to thirdwebstorage-dev.com/ replacement is temporary
|
||||
// This should be fixed in the insight dev endpoint
|
||||
|
||||
const walletNFTs = nftsResponse.data.map((nft) => {
|
||||
const walletNFT: WalletNFT = {
|
||||
id: nft.token_id,
|
||||
contractAddress: nft.contract.address,
|
||||
metadata: {
|
||||
uri: isDev
|
||||
? nft.metadata_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
|
||||
: nft.metadata_url,
|
||||
name: nft.name,
|
||||
description: nft.description,
|
||||
image: isDev
|
||||
? nft.image_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/")
|
||||
: nft.image_url,
|
||||
animation_url: isDev
|
||||
? nft.extra_metadata.animation_original_url?.replace(
|
||||
"ipfscdn.io/",
|
||||
"thirdwebstorage-dev.com/",
|
||||
)
|
||||
: nft.extra_metadata.animation_original_url,
|
||||
external_url: nft.external_url,
|
||||
background_color: nft.background_color,
|
||||
},
|
||||
owner: params.owner,
|
||||
tokenURI: nft.metadata_url,
|
||||
type: nft.token_type === "erc721" ? "ERC721" : "ERC1155",
|
||||
supply: nft.balance,
|
||||
};
|
||||
|
||||
return walletNFT;
|
||||
});
|
||||
|
||||
return {
|
||||
data: walletNFTs,
|
||||
ok: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,13 +5,14 @@ import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
type ProxyActionParams = {
|
||||
pathname: string;
|
||||
searchParams?: Record<string, string>;
|
||||
searchParams?: Record<string, string | undefined>;
|
||||
method: "GET" | "POST" | "PUT" | "DELETE";
|
||||
body?: string;
|
||||
headers?: Record<string, string>;
|
||||
parseAsText?: boolean;
|
||||
};
|
||||
|
||||
type ProxyActionResult<T extends object> =
|
||||
type ProxyActionResult<T> =
|
||||
| {
|
||||
status: number;
|
||||
ok: true;
|
||||
@@ -23,7 +24,7 @@ type ProxyActionResult<T extends object> =
|
||||
error: string;
|
||||
};
|
||||
|
||||
async function proxy<T extends object>(
|
||||
async function proxy<T>(
|
||||
baseUrl: string,
|
||||
params: ProxyActionParams,
|
||||
): Promise<ProxyActionResult<T>> {
|
||||
@@ -34,7 +35,10 @@ async function proxy<T extends object>(
|
||||
url.pathname = params.pathname;
|
||||
if (params.searchParams) {
|
||||
for (const key in params.searchParams) {
|
||||
url.searchParams.append(key, params.searchParams[key] as string);
|
||||
const value = params.searchParams[key];
|
||||
if (value) {
|
||||
url.searchParams.append(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,19 +71,15 @@ async function proxy<T extends object>(
|
||||
return {
|
||||
status: res.status,
|
||||
ok: true,
|
||||
data: await res.json(),
|
||||
data: params.parseAsText ? await res.text() : await res.json(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function apiServerProxy<T extends object = object>(
|
||||
params: ProxyActionParams,
|
||||
) {
|
||||
export async function apiServerProxy<T>(params: ProxyActionParams) {
|
||||
return proxy<T>(API_SERVER_URL, params);
|
||||
}
|
||||
|
||||
export async function payServerProxy<T extends object = object>(
|
||||
params: ProxyActionParams,
|
||||
) {
|
||||
export async function payServerProxy<T>(params: ProxyActionParams) {
|
||||
return proxy<T>(
|
||||
process.env.NEXT_PUBLIC_PAY_URL
|
||||
? `https://${process.env.NEXT_PUBLIC_PAY_URL}`
|
||||
@@ -87,3 +87,7 @@ export async function payServerProxy<T extends object = object>(
|
||||
params,
|
||||
);
|
||||
}
|
||||
|
||||
export async function analyticsServerProxy<T>(params: ProxyActionParams) {
|
||||
return proxy<T>(process.env.ANALYTICS_SERVICE_URL || "", params);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import "server-only";
|
||||
|
||||
import Stripe from "stripe";
|
||||
import type { Team } from "../api/team";
|
||||
|
||||
let existingStripe: Stripe | undefined;
|
||||
|
||||
function getStripe() {
|
||||
if (!existingStripe) {
|
||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
|
||||
|
||||
if (!STRIPE_SECRET_KEY) {
|
||||
throw new Error("STRIPE_SECRET_KEY is not set");
|
||||
}
|
||||
|
||||
existingStripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||
apiVersion: "2025-02-24.acacia",
|
||||
});
|
||||
}
|
||||
|
||||
return existingStripe;
|
||||
}
|
||||
|
||||
export async function getTeamInvoices(
|
||||
team: Team,
|
||||
options?: { cursor?: string },
|
||||
) {
|
||||
try {
|
||||
const customerId = team.stripeCustomerId;
|
||||
|
||||
if (!customerId) {
|
||||
throw new Error("No customer ID found");
|
||||
}
|
||||
|
||||
// Get the list of invoices for the customer
|
||||
const invoices = await getStripe().invoices.list({
|
||||
customer: customerId,
|
||||
limit: 10,
|
||||
starting_after: options?.cursor,
|
||||
});
|
||||
|
||||
return invoices;
|
||||
} catch (error) {
|
||||
console.error("Error fetching billing history:", error);
|
||||
|
||||
// If the error is that the customer doesn't exist, return an empty array
|
||||
// instead of throwing an error
|
||||
if (
|
||||
error instanceof Stripe.errors.StripeError &&
|
||||
error.message.includes("No such customer")
|
||||
) {
|
||||
return {
|
||||
data: [],
|
||||
has_more: false,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error("Failed to fetch billing history");
|
||||
}
|
||||
}
|
||||
@@ -270,9 +270,10 @@ type ActiveStatus = {
|
||||
ecosystemWallet: boolean;
|
||||
};
|
||||
|
||||
export async function isProjectActive(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<ActiveStatus> {
|
||||
export async function isProjectActive(params: {
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
}): Promise<ActiveStatus> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(
|
||||
`v2/active-usage?${searchParams.toString()}`,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { API_SERVER_URL, THIRDWEB_API_SECRET } from "../constants/env";
|
||||
|
||||
export type SMSCountryTiers = {
|
||||
tier1: string[];
|
||||
tier2: string[];
|
||||
tier3: string[];
|
||||
tier4: string[];
|
||||
tier5: string[];
|
||||
};
|
||||
|
||||
export async function getSMSCountryTiers() {
|
||||
if (!THIRDWEB_API_SECRET) {
|
||||
throw new Error("API_SERVER_SECRET is not set");
|
||||
}
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/sms/list-country-tiers`, {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-service-api-key": THIRDWEB_API_SECRET,
|
||||
},
|
||||
next: {
|
||||
revalidate: 15 * 60, //15 minutes
|
||||
},
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
console.error(
|
||||
"Failed to fetch sms country tiers",
|
||||
res.status,
|
||||
res.statusText,
|
||||
);
|
||||
res.body?.cancel();
|
||||
return {
|
||||
tier1: [],
|
||||
tier2: [],
|
||||
tier3: [],
|
||||
tier4: [],
|
||||
tier5: [],
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
return (await res.json()).data as SMSCountryTiers;
|
||||
} catch (e) {
|
||||
console.error("Failed to parse sms country tiers", e);
|
||||
return {
|
||||
tier1: [],
|
||||
tier2: [],
|
||||
tier3: [],
|
||||
tier4: [],
|
||||
tier5: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ import { API_SERVER_URL, THIRDWEB_API_SECRET } from "@/constants/env";
|
||||
import type { TeamResponse } from "@thirdweb-dev/service-utils";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
|
||||
export type Team = TeamResponse;
|
||||
export type Team = TeamResponse & { stripeCustomerId: string | null };
|
||||
|
||||
export async function getTeamBySlug(slug: string) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function TextDivider(props: {
|
||||
text: string;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center text-muted-foreground text-sm",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<span className="h-[1px] flex-1 bg-border" />
|
||||
<span className="mx-4">{props.text}</span>
|
||||
<span className="h-[1px] flex-1 bg-border" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,78 +1,136 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { toast } from "sonner";
|
||||
import type {
|
||||
BillingBillingPortalAction,
|
||||
BillingPortalOptions,
|
||||
RedirectBillingCheckoutAction,
|
||||
RedirectCheckoutOptions,
|
||||
GetBillingCheckoutUrlAction,
|
||||
GetBillingCheckoutUrlOptions,
|
||||
GetBillingPortalUrlAction,
|
||||
GetBillingPortalUrlOptions,
|
||||
} from "../actions/billing";
|
||||
import { cn } from "../lib/utils";
|
||||
import { Spinner } from "./ui/Spinner/Spinner";
|
||||
import { Button, type ButtonProps } from "./ui/button";
|
||||
|
||||
type CheckoutButtonProps = Omit<RedirectCheckoutOptions, "redirectUrl"> &
|
||||
ButtonProps & {
|
||||
redirectPath: string;
|
||||
redirectToCheckout: RedirectBillingCheckoutAction;
|
||||
};
|
||||
type CheckoutButtonProps = Omit<GetBillingCheckoutUrlOptions, "redirectUrl"> & {
|
||||
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
|
||||
buttonProps?: Omit<ButtonProps, "children">;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function CheckoutButton({
|
||||
onClick,
|
||||
teamSlug,
|
||||
sku,
|
||||
metadata,
|
||||
redirectPath,
|
||||
getBillingCheckoutUrl,
|
||||
children,
|
||||
redirectToCheckout,
|
||||
...restProps
|
||||
buttonProps,
|
||||
}: CheckoutButtonProps) {
|
||||
const getUrlMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return getBillingCheckoutUrl({
|
||||
teamSlug,
|
||||
sku,
|
||||
metadata,
|
||||
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const errorMessage = "Failed to open checkout page";
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...restProps}
|
||||
{...buttonProps}
|
||||
className={cn(buttonProps?.className, "gap-2")}
|
||||
disabled={getUrlMutation.isPending || buttonProps?.disabled}
|
||||
onClick={async (e) => {
|
||||
onClick?.(e);
|
||||
await redirectToCheckout({
|
||||
teamSlug,
|
||||
sku,
|
||||
metadata,
|
||||
redirectUrl: getRedirectUrl(redirectPath),
|
||||
buttonProps?.onClick?.(e);
|
||||
getUrlMutation.mutate(undefined, {
|
||||
onSuccess: (res) => {
|
||||
if (!res.url) {
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = window.open(res.url, "_blank");
|
||||
|
||||
if (!tab) {
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(errorMessage);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{getUrlMutation.isPending && <Spinner className="size-4" />}
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
type BillingPortalButtonProps = Omit<BillingPortalOptions, "redirectUrl"> &
|
||||
ButtonProps & {
|
||||
redirectPath: string;
|
||||
redirectToBillingPortal: BillingBillingPortalAction;
|
||||
};
|
||||
type BillingPortalButtonProps = Omit<
|
||||
GetBillingPortalUrlOptions,
|
||||
"redirectUrl"
|
||||
> & {
|
||||
getBillingPortalUrl: GetBillingPortalUrlAction;
|
||||
buttonProps?: Omit<ButtonProps, "children">;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function BillingPortalButton({
|
||||
onClick,
|
||||
teamSlug,
|
||||
redirectPath,
|
||||
children,
|
||||
redirectToBillingPortal,
|
||||
...restProps
|
||||
getBillingPortalUrl,
|
||||
buttonProps,
|
||||
}: BillingPortalButtonProps) {
|
||||
const getUrlMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
return getBillingPortalUrl({
|
||||
teamSlug,
|
||||
redirectUrl: getAbsoluteUrl("/stripe-redirect"),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const errorMessage = "Failed to open billing portal";
|
||||
|
||||
return (
|
||||
<Button
|
||||
{...restProps}
|
||||
{...buttonProps}
|
||||
className={cn(buttonProps?.className, "gap-2")}
|
||||
disabled={getUrlMutation.isPending || buttonProps?.disabled}
|
||||
onClick={async (e) => {
|
||||
onClick?.(e);
|
||||
await redirectToBillingPortal({
|
||||
teamSlug,
|
||||
redirectUrl: getRedirectUrl(redirectPath),
|
||||
buttonProps?.onClick?.(e);
|
||||
getUrlMutation.mutate(undefined, {
|
||||
onSuccess(res) {
|
||||
if (!res.url) {
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
const tab = window.open(res.url, "_blank");
|
||||
if (!tab) {
|
||||
toast.error(errorMessage);
|
||||
return;
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error(errorMessage);
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
{getUrlMutation.isPending && <Spinner className="size-4" />}
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
function getRedirectUrl(path: string) {
|
||||
function getAbsoluteUrl(path: string) {
|
||||
const url = new URL(window.location.origin);
|
||||
url.pathname = path;
|
||||
return url.toString();
|
||||
|
||||
@@ -8,13 +8,12 @@ import { GradientAvatar } from "./GradientAvatar";
|
||||
const meta = {
|
||||
title: "blocks/Avatars/GradientAvatar",
|
||||
component: Story,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
@@ -22,14 +21,14 @@ const client = getThirdwebClient();
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 p-10">
|
||||
<p> All images below are set with size-6 className </p>
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<p> All images below are set with size-20 className </p>
|
||||
|
||||
<BadgeContainer label="No Src, No id - Skeleton">
|
||||
<GradientAvatar
|
||||
id={undefined}
|
||||
src={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -38,7 +37,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
id={"foo"}
|
||||
src={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -47,7 +46,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
id={"foo"}
|
||||
src={""}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -56,7 +55,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
id={"bar"}
|
||||
src={""}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -65,7 +64,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
src="invalid-src"
|
||||
id={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -74,7 +73,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
src="https://picsum.photos/200/300"
|
||||
id={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -83,7 +82,7 @@ function Story() {
|
||||
<GradientAvatar
|
||||
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
|
||||
id={undefined}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -123,14 +122,14 @@ function ToggleTest() {
|
||||
<GradientAvatar
|
||||
src={data?.src}
|
||||
id={data?.id}
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<GradientAvatar
|
||||
className="size-6"
|
||||
className="size-20"
|
||||
src={data ? "invalid-src" : undefined}
|
||||
id={undefined}
|
||||
client={client}
|
||||
|
||||
@@ -14,7 +14,7 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
@@ -22,7 +22,7 @@ const client = getThirdwebClient();
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 p-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<p> All images below are set with size-6 className </p>
|
||||
|
||||
<BadgeContainer label="No Src - Skeleton">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { DangerSettingCard } from "./DangerSettingCard";
|
||||
|
||||
const meta = {
|
||||
@@ -15,17 +15,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
|
||||
|
||||
@@ -2,11 +2,12 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
import Papa from "papaparse";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export function ExportToCSVButton(props: {
|
||||
getData: () => Promise<{ header: string[]; rows: string[][] }>;
|
||||
getData: () => Promise<{ header: string[]; rows: string[][] } | string>;
|
||||
fileName: string;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
@@ -14,7 +15,12 @@ export function ExportToCSVButton(props: {
|
||||
const exportMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
const data = await props.getData();
|
||||
exportToCSV(props.fileName, data);
|
||||
if (typeof data === "string") {
|
||||
exportToCSV(props.fileName, data);
|
||||
} else {
|
||||
const fileContent = convertToCSVFormat(data);
|
||||
exportToCSV(props.fileName, fileContent);
|
||||
}
|
||||
},
|
||||
onError: () => {
|
||||
toast.error("Failed to download CSV");
|
||||
@@ -29,7 +35,7 @@ export function ExportToCSVButton(props: {
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={props.disabled}
|
||||
disabled={props.disabled || exportMutation.isPending}
|
||||
className={cn("flex items-center gap-2 border text-xs", props.className)}
|
||||
onClick={async () => {
|
||||
exportMutation.mutate();
|
||||
@@ -50,15 +56,15 @@ export function ExportToCSVButton(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function exportToCSV(
|
||||
fileName: string,
|
||||
data: { header: string[]; rows: string[][] },
|
||||
) {
|
||||
const { header, rows } = data;
|
||||
const csvContent = `data:text/csv;charset=utf-8,${header.join(",")}\n${rows
|
||||
.map((e) => e.join(","))
|
||||
.join("\n")}`;
|
||||
function convertToCSVFormat(data: { header: string[]; rows: string[][] }) {
|
||||
return Papa.unparse({
|
||||
fields: data.header,
|
||||
data: data.rows,
|
||||
});
|
||||
}
|
||||
|
||||
function exportToCSV(fileName: string, fileContent: string) {
|
||||
const csvContent = `data:text/csv;charset=utf-8,${fileContent}`;
|
||||
const encodedUri = encodeURI(csvContent);
|
||||
const link = document.createElement("a");
|
||||
link.setAttribute("href", encodedUri);
|
||||
|
||||
@@ -9,13 +9,12 @@ import { Img } from "./Img";
|
||||
const meta = {
|
||||
title: "blocks/Img",
|
||||
component: Story,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ export function Img(props: imgElementProps) {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative shrink-0">
|
||||
<img
|
||||
{...restProps}
|
||||
// avoid setting empty src string to prevent request to the entire page
|
||||
|
||||
@@ -19,30 +19,7 @@ export function MobileSidebar(props: {
|
||||
triggerClassName?: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
|
||||
const activeLink = useMemo(() => {
|
||||
function isActive(link: SidebarBaseLink) {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
|
||||
for (const link of props.links) {
|
||||
if ("group" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.links, pathname]);
|
||||
const activeLink = useActiveSidebarLink(props.links);
|
||||
|
||||
const defaultTrigger = (
|
||||
<Button
|
||||
@@ -75,3 +52,32 @@ export function MobileSidebar(props: {
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export function useActiveSidebarLink(links: SidebarLink[]) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const activeLink = useMemo(() => {
|
||||
function isActive(link: SidebarBaseLink) {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
|
||||
for (const link of links) {
|
||||
if ("group" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
}
|
||||
}
|
||||
} else if ("href" in link) {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [links, pathname]);
|
||||
|
||||
return activeLink;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { MultiNetworkSelector } from "./NetworkSelectors";
|
||||
|
||||
const meta = {
|
||||
@@ -16,20 +16,13 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-8 py-10">
|
||||
<Variant label="No Chains selected by default" selectedChainIds={[]} />
|
||||
<Variant
|
||||
label="Polygon, Ethereum selected by default"
|
||||
|
||||
@@ -18,17 +18,39 @@ export function MultiNetworkSelector(props: {
|
||||
onChange: (chainIds: number[]) => void;
|
||||
disableChainId?: boolean;
|
||||
className?: string;
|
||||
priorityChains?: number[];
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
const options = useMemo(() => {
|
||||
return allChains.map((chain) => {
|
||||
let sortedChains = allChains;
|
||||
|
||||
if (props.priorityChains) {
|
||||
const priorityChainsSet = new Set();
|
||||
for (const chainId of props.priorityChains || []) {
|
||||
priorityChainsSet.add(chainId);
|
||||
}
|
||||
|
||||
const priorityChains = (props.priorityChains || [])
|
||||
.map((chainId) => {
|
||||
return idToChain.get(chainId);
|
||||
})
|
||||
.filter((v) => !!v);
|
||||
|
||||
const otherChains = allChains.filter(
|
||||
(chain) => !priorityChainsSet.has(chain.chainId),
|
||||
);
|
||||
|
||||
sortedChains = [...priorityChains, ...otherChains];
|
||||
}
|
||||
|
||||
return sortedChains.map((chain) => {
|
||||
return {
|
||||
label: cleanChainName(chain.name),
|
||||
value: String(chain.chainId),
|
||||
};
|
||||
});
|
||||
}, [allChains]);
|
||||
}, [allChains, props.priorityChains, idToChain]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
@@ -104,6 +126,7 @@ export function SingleNetworkSelector(props: {
|
||||
side?: "left" | "right" | "top" | "bottom";
|
||||
disableChainId?: boolean;
|
||||
align?: "center" | "start" | "end";
|
||||
placeholder?: string;
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
@@ -180,7 +203,11 @@ export function SingleNetworkSelector(props: {
|
||||
props.onChange(Number(chainId));
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
placeholder={isLoadingChains ? "Loading Chains..." : "Select Chain"}
|
||||
placeholder={
|
||||
isLoadingChains
|
||||
? "Loading Chains..."
|
||||
: props.placeholder || "Select Chain"
|
||||
}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { SettingsCard } from "./SettingsCard";
|
||||
|
||||
const meta = {
|
||||
@@ -15,139 +15,130 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="min-h-dvh bg-background py-6 text-foreground">
|
||||
<div className="container flex max-w-[1100px] flex-col gap-10 lg:p-10">
|
||||
<BadgeContainer label="No Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={undefined}
|
||||
errorText={undefined}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<BadgeContainer label="No Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={undefined}
|
||||
errorText={undefined}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText={undefined}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText={undefined}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Error">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText="This is error text"
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="Error">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText="This is error text"
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Permission">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText="You do not have permission to edit this"
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="No Permission">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText="You do not have permission to edit this"
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Save">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="Save">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Save Disabled">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: true,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
<BadgeContainer label="Save Disabled">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: true,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Save Loading">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: true,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
<BadgeContainer label="Save Loading">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: true,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,11 +2,13 @@ import { ExternalLinkIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
import { Separator } from "../ui/separator";
|
||||
|
||||
export type SidebarBaseLink = {
|
||||
href: string;
|
||||
label: React.ReactNode;
|
||||
exactMatch?: boolean;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
tracking?: {
|
||||
category: string;
|
||||
action: string;
|
||||
@@ -19,6 +21,9 @@ export type SidebarLink =
|
||||
| {
|
||||
group: string;
|
||||
links: SidebarBaseLink[];
|
||||
}
|
||||
| {
|
||||
separator: true;
|
||||
};
|
||||
|
||||
type SidebarContentProps = {
|
||||
@@ -27,21 +32,18 @@ type SidebarContentProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function Sidebar(props: SidebarContentProps) {
|
||||
export function CustomSidebar(props: SidebarContentProps) {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"sticky top-0 hidden w-[230px] flex-shrink-0 self-start lg:block",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="py-7">
|
||||
{props.header}
|
||||
<div className="flex flex-col gap-1">
|
||||
<RenderSidebarLinks links={props.links} />
|
||||
<div className={cn("hidden w-[230px] shrink-0 lg:block", props.className)}>
|
||||
<aside className="sticky top-0 self-start">
|
||||
<div className="py-7">
|
||||
{props.header}
|
||||
<div className="flex flex-col gap-1">
|
||||
<RenderSidebarLinks links={props.links} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</aside>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -61,6 +63,10 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
);
|
||||
}
|
||||
|
||||
if ("separator" in link) {
|
||||
return <Separator className="my-2" />;
|
||||
}
|
||||
|
||||
const isExternal = link.href.startsWith("http");
|
||||
return (
|
||||
<NavLink
|
||||
@@ -68,9 +74,10 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
key={i}
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
activeClassName="text-foreground"
|
||||
activeClassName="text-foreground bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
{link.label}
|
||||
{isExternal && <ExternalLinkIcon className="size-3" />}
|
||||
</NavLink>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { mobileViewport } from "../../../stories/utils";
|
||||
import { SidebarLayout } from "./SidebarLayout";
|
||||
|
||||
const meta = {
|
||||
@@ -15,17 +14,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<SidebarLayout
|
||||
|
||||
@@ -1,6 +1,23 @@
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupContent,
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { MobileSidebar } from "./MobileSidebar";
|
||||
import { Sidebar, type SidebarLink } from "./Sidebar";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
|
||||
import { CustomSidebar, type SidebarLink } from "./Sidebar";
|
||||
import { AppFooter } from "./app-footer";
|
||||
|
||||
export function SidebarLayout(props: {
|
||||
sidebarLinks: SidebarLink[];
|
||||
@@ -17,7 +34,10 @@ export function SidebarLayout(props: {
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<Sidebar links={sidebarLinks} className={props.desktopSidebarClassName} />
|
||||
<CustomSidebar
|
||||
links={sidebarLinks}
|
||||
className={props.desktopSidebarClassName}
|
||||
/>
|
||||
<MobileSidebar
|
||||
links={sidebarLinks}
|
||||
triggerClassName={props.mobileSidebarClassName}
|
||||
@@ -29,3 +49,117 @@ export function SidebarLayout(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function FullWidthSidebarLayout(props: {
|
||||
contentSidebarLinks: SidebarLink[];
|
||||
footerSidebarLinks?: SidebarLink[];
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
const { contentSidebarLinks, children, footerSidebarLinks } = props;
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 overflow-y-hidden",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
{/* left - sidebar */}
|
||||
<Sidebar collapsible="icon" side="left" className="pt-2">
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<RenderSidebarGroup
|
||||
sidebarLinks={contentSidebarLinks}
|
||||
groupName={undefined}
|
||||
/>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
|
||||
{footerSidebarLinks && (
|
||||
<SidebarFooter className="pb-3">
|
||||
<RenderSidebarGroup
|
||||
sidebarLinks={footerSidebarLinks}
|
||||
groupName={undefined}
|
||||
/>
|
||||
</SidebarFooter>
|
||||
)}
|
||||
|
||||
<SidebarRail />
|
||||
</Sidebar>
|
||||
|
||||
{/* right - content */}
|
||||
<div className="flex h-full flex-grow flex-col overflow-y-auto">
|
||||
<MobileSidebarTrigger
|
||||
links={[...contentSidebarLinks, ...(footerSidebarLinks || [])]}
|
||||
/>
|
||||
|
||||
<main className="z-0 flex min-w-0 grow flex-col max-sm:w-full">
|
||||
{children}
|
||||
</main>
|
||||
<AppFooter containerClassName="max-w-7xl" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RenderSidebarGroup(props: {
|
||||
sidebarLinks: SidebarLink[];
|
||||
groupName: string | undefined;
|
||||
}) {
|
||||
const { sidebarLinks } = props;
|
||||
const sidebar = useSidebar();
|
||||
|
||||
return (
|
||||
<SidebarMenu className="gap-1.5">
|
||||
{sidebarLinks.map((link) => {
|
||||
if ("href" in link) {
|
||||
return (
|
||||
<SidebarMenuItem key={link.href}>
|
||||
<SidebarMenuButton asChild>
|
||||
<NavLink
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
activeClassName="text-foreground bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
tracking={link.tracking}
|
||||
onClick={() => {
|
||||
sidebar.setOpenMobile(false);
|
||||
}}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
<span>{link.label}</span>
|
||||
</NavLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
if ("separator" in link) {
|
||||
return <SidebarSeparator className="my-1" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderSidebarGroup
|
||||
sidebarLinks={link.links}
|
||||
groupName={link.group}
|
||||
key={link.group}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SidebarMenu>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
|
||||
const activeLink = useActiveSidebarLink(props.links);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
<SidebarTrigger className="size-4" />
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
{activeLink && <span className="text-sm">{activeLink.label}</span>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { SingleNetworkSelector } from "./NetworkSelectors";
|
||||
|
||||
const meta = {
|
||||
@@ -16,20 +16,13 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-8 py-10">
|
||||
<Variant label="No Chain ID selected by default" chainId={undefined} />
|
||||
<Variant label="Polygon selected by default" chainId={137} />
|
||||
<Variant
|
||||
|
||||
@@ -12,17 +12,23 @@ import Link from "next/link";
|
||||
|
||||
type AppFooterProps = {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
export function AppFooter(props: AppFooterProps) {
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
"w-full border-border border-t bg-card py-6 md:py-8",
|
||||
"w-full border-border border-t py-6 md:py-8",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="container flex flex-col gap-4 md:gap-6">
|
||||
<div
|
||||
className={cn(
|
||||
"container flex flex-col gap-4 md:gap-6",
|
||||
props.containerClassName,
|
||||
)}
|
||||
>
|
||||
{/* top row */}
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
|
||||
@@ -30,6 +30,7 @@ type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
description?: string;
|
||||
titleClassName?: string;
|
||||
};
|
||||
customHeader?: React.ReactNode;
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
@@ -60,6 +61,8 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
{props.customHeader && props.customHeader}
|
||||
|
||||
<CardContent className={cn(!props.header && "pt-6")}>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
{props.isPending ? (
|
||||
|
||||
@@ -32,6 +32,7 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
description?: string;
|
||||
titleClassName?: string;
|
||||
};
|
||||
customHeader?: React.ReactNode;
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
@@ -41,7 +42,9 @@ type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
chartClassName?: string;
|
||||
isPending: boolean;
|
||||
toolTipLabelFormatter?: (label: string, payload: unknown) => React.ReactNode;
|
||||
toolTipValueFormatter?: (value: unknown) => React.ReactNode;
|
||||
hideLabel?: boolean;
|
||||
emptyChartState?: React.ReactElement;
|
||||
};
|
||||
|
||||
export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
@@ -65,12 +68,14 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
</CardHeader>
|
||||
)}
|
||||
|
||||
{props.customHeader && props.customHeader}
|
||||
|
||||
<CardContent className={cn(!props.header && "pt-6")}>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
<EmptyChartState />
|
||||
<EmptyChartState>{props.emptyChartState}</EmptyChartState>
|
||||
) : (
|
||||
<BarChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
@@ -88,6 +93,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
props.hideLabel !== undefined ? props.hideLabel : true
|
||||
}
|
||||
labelFormatter={props.toolTipLabelFormatter}
|
||||
valueFormatter={props.toolTipValueFormatter}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
@@ -96,25 +102,15 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
content={<ChartLegendContent className="pt-5" />}
|
||||
/>
|
||||
)}
|
||||
{configKeys.map((key, idx) => (
|
||||
{configKeys.map((key) => (
|
||||
<Bar
|
||||
key={key}
|
||||
dataKey={key}
|
||||
// if stacked then they should all be the same stackId
|
||||
// if grouped then they should all be unique stackId (so the key works great)
|
||||
stackId={variant === "stacked" ? "a" : key}
|
||||
fill={`var(--color-${key})`}
|
||||
// if stacked then we need to figure out the radius based on the index in the array
|
||||
// if grouped then we can just use the same radius for all
|
||||
radius={
|
||||
variant === "stacked"
|
||||
? idx === 0
|
||||
? [0, 0, 4, 4]
|
||||
: idx === configKeys.length - 1
|
||||
? [4, 4, 0, 0]
|
||||
: [0, 0, 0, 0]
|
||||
: [4, 4, 4, 4]
|
||||
}
|
||||
fill={props.config[key]?.color}
|
||||
radius={[4, 4, 4, 4]}
|
||||
strokeWidth={1}
|
||||
className="stroke-background"
|
||||
/>
|
||||
|
||||
@@ -96,7 +96,6 @@ export const CodeSegment: React.FC<CodeSegmentProps> = ({
|
||||
onClick: () => setEnvironment(env.environment),
|
||||
isActive: activeEnvironment === env.environment,
|
||||
name: env.title,
|
||||
isEnabled: true,
|
||||
}))}
|
||||
tabClassName="text-sm gap-2 !text-sm"
|
||||
tabIconClassName="size-4"
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { type CodeEnvironment, CodeSegment } from "./code-segment.client";
|
||||
|
||||
const meta = {
|
||||
@@ -24,17 +24,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
type Mode = "default" | "no-tabs" | "only-tabs";
|
||||
|
||||
function Story() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { MultiSelect } from "./multi-select";
|
||||
|
||||
const meta = {
|
||||
@@ -16,17 +16,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function createList(len: number) {
|
||||
return Array.from({ length: len }, (_, i) => ({
|
||||
value: `${i}`,
|
||||
@@ -36,7 +29,7 @@ function createList(len: number) {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
|
||||
<div className="container flex max-w-6xl flex-col gap-6 py-10">
|
||||
<VariantTest defaultValues={[]} storyLabel="5 items" listLen={5} />
|
||||
<VariantTest defaultValues={[]} storyLabel="5000 items" listLen={5000} />
|
||||
<VariantTest
|
||||
|
||||
@@ -1,40 +1,42 @@
|
||||
"use client";
|
||||
import type { Team } from "@/api/team";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, CircleAlertIcon, CircleDollarSignIcon } from "lucide-react";
|
||||
import { CheckIcon, CircleDollarSignIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { TEAM_PLANS } from "utils/pricing";
|
||||
import { useTrack } from "../../../hooks/analytics/useTrack";
|
||||
import { remainingDays } from "../../../utils/date-utils";
|
||||
import type { RedirectBillingCheckoutAction } from "../../actions/billing";
|
||||
import type { GetBillingCheckoutUrlAction } from "../../actions/billing";
|
||||
import type { ProductSKU } from "../../lib/billing";
|
||||
import { CheckoutButton } from "../billing";
|
||||
|
||||
type ButtonProps = React.ComponentProps<typeof Button>;
|
||||
|
||||
const PRO_CONTACT_US_URL =
|
||||
"https://meetings.hubspot.com/sales-thirdweb/thirdweb-pro";
|
||||
type PricingCardCta = {
|
||||
hint?: string;
|
||||
title: string;
|
||||
onClick?: () => void;
|
||||
} & (
|
||||
| {
|
||||
type: "link";
|
||||
href: string;
|
||||
}
|
||||
| {
|
||||
type: "checkout";
|
||||
}
|
||||
);
|
||||
|
||||
type PricingCardProps = {
|
||||
teamSlug: string;
|
||||
billingPlan: Exclude<Team["billingPlan"], "free">;
|
||||
cta?: {
|
||||
hint?: string;
|
||||
title: string;
|
||||
tracking: {
|
||||
category: string;
|
||||
label?: string;
|
||||
};
|
||||
variant?: ButtonProps["variant"];
|
||||
};
|
||||
billingPlan: keyof typeof TEAM_PLANS;
|
||||
cta?: PricingCardCta;
|
||||
ctaHint?: string;
|
||||
highlighted?: boolean;
|
||||
current?: boolean;
|
||||
canTrialGrowth?: boolean;
|
||||
activeTrialEndsAt?: string;
|
||||
redirectPath: string;
|
||||
redirectToCheckout: RedirectBillingCheckoutAction;
|
||||
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
|
||||
};
|
||||
|
||||
export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
@@ -43,21 +45,29 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
cta,
|
||||
highlighted = false,
|
||||
current = false,
|
||||
canTrialGrowth = false,
|
||||
activeTrialEndsAt,
|
||||
redirectPath,
|
||||
redirectToCheckout,
|
||||
getBillingCheckoutUrl,
|
||||
}) => {
|
||||
const plan = TEAM_PLANS[billingPlan];
|
||||
const isCustomPrice = typeof plan.price === "string";
|
||||
|
||||
const trackEvent = useTrack();
|
||||
const remainingTrialDays =
|
||||
(activeTrialEndsAt ? remainingDays(activeTrialEndsAt) : 0) || 0;
|
||||
|
||||
const handleCTAClick = () => {
|
||||
cta?.onClick?.();
|
||||
trackEvent({
|
||||
category: "account",
|
||||
label: `${billingPlan}Plan`,
|
||||
action: "click",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"z-10 flex w-full flex-col gap-6 rounded-xl border border-border bg-card p-4 md:p-6",
|
||||
"z-10 flex w-full flex-col gap-4 rounded-xl border border-border bg-card p-4",
|
||||
current && "border-blue-500",
|
||||
highlighted && "border-active-border",
|
||||
)}
|
||||
@@ -73,13 +83,13 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* Title + Desc */}
|
||||
<div>
|
||||
<div className="mb-2 flex flex-row items-center gap-2">
|
||||
<div className="mb-1 flex flex-row items-center gap-3">
|
||||
<h3 className="font-semibold text-2xl capitalize tracking-tight">
|
||||
{plan.title}
|
||||
</h3>
|
||||
{current && <Badge className="capitalize">Current plan</Badge>}
|
||||
</div>
|
||||
<p className="max-w-[320px] text-muted-foreground">
|
||||
<p className="max-w-[320px] text-muted-foreground text-sm">
|
||||
{plan.description}
|
||||
</p>
|
||||
</div>
|
||||
@@ -87,41 +97,13 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
{/* Price */}
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-semibold text-3xl text-foreground tracking-tight">
|
||||
{isCustomPrice ? (
|
||||
plan.price
|
||||
) : canTrialGrowth ? (
|
||||
<>
|
||||
<span className="text-muted-foreground line-through">
|
||||
${plan.price}
|
||||
</span>{" "}
|
||||
$0
|
||||
</>
|
||||
) : (
|
||||
`$${plan.price}`
|
||||
)}
|
||||
<span className="font-semibold text-2xl text-foreground tracking-tight">
|
||||
${plan.price}
|
||||
</span>
|
||||
|
||||
{!isCustomPrice && (
|
||||
<span className="text-muted-foreground">/ month</span>
|
||||
)}
|
||||
|
||||
{billingPlan === "starter" && (
|
||||
<ToolTipLabel
|
||||
contentClassName="max-w-[320px]"
|
||||
label="We will place a temporary hold of $25 to verify your card, this will be immediately released back to you after verification."
|
||||
>
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
className="h-auto w-auto p-1 text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<div>
|
||||
<CircleAlertIcon className="size-5 shrink-0" />
|
||||
</div>
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{remainingTrialDays > 0 && (
|
||||
@@ -135,9 +117,9 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex grow flex-col items-start gap-2.5 text-foreground">
|
||||
<div className="flex grow flex-col items-start gap-2 text-foreground">
|
||||
{plan.subTitle && (
|
||||
<p className="font-medium text-foreground">{plan.subTitle}</p>
|
||||
<p className="font-medium text-foreground text-sm">{plan.subTitle}</p>
|
||||
)}
|
||||
|
||||
{plan.features.map((f) => (
|
||||
@@ -147,26 +129,30 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
|
||||
{cta && (
|
||||
<div className="flex flex-col gap-3">
|
||||
{billingPlan !== "pro" ? (
|
||||
{billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && (
|
||||
<CheckoutButton
|
||||
variant={cta.variant || "outline"}
|
||||
buttonProps={{
|
||||
variant: highlighted ? "default" : "outline",
|
||||
className: highlighted ? undefined : "bg-background",
|
||||
onClick: handleCTAClick,
|
||||
}}
|
||||
teamSlug={teamSlug}
|
||||
sku={billingPlan === "starter" ? "plan:starter" : "plan:growth"}
|
||||
redirectPath={redirectPath}
|
||||
redirectToCheckout={redirectToCheckout}
|
||||
sku={billingPlanToSkuMap[billingPlan]}
|
||||
getBillingCheckoutUrl={getBillingCheckoutUrl}
|
||||
>
|
||||
{cta.title}
|
||||
</CheckoutButton>
|
||||
) : (
|
||||
<Button variant={cta.variant || "outline"} asChild>
|
||||
<TrackedLinkTW
|
||||
href={PRO_CONTACT_US_URL}
|
||||
label={cta.tracking?.label}
|
||||
category={cta.tracking?.category}
|
||||
target="_blank"
|
||||
>
|
||||
)}
|
||||
|
||||
{cta.type === "link" && (
|
||||
<Button
|
||||
variant={highlighted ? "default" : "outline"}
|
||||
className={highlighted ? undefined : "bg-background"}
|
||||
asChild
|
||||
>
|
||||
<Link href={cta.href} target="_blank" onClick={handleCTAClick}>
|
||||
{cta.title}
|
||||
</TrackedLinkTW>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -181,6 +167,19 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
const billingPlanToSkuMap: Record<Team["billingPlan"], ProductSKU | undefined> =
|
||||
{
|
||||
starter: "plan:starter",
|
||||
growth: "plan:growth",
|
||||
accelerate: "plan:accelerate",
|
||||
scale: "plan:scale",
|
||||
// we can't render checkout buttons for these plans:
|
||||
pro: undefined,
|
||||
free: undefined,
|
||||
growth_legacy: undefined,
|
||||
starter_legacy: undefined,
|
||||
};
|
||||
|
||||
type FeatureItemProps = {
|
||||
text: string | string[];
|
||||
};
|
||||
@@ -189,7 +188,7 @@ function FeatureItem({ text }: FeatureItemProps) {
|
||||
const titleStr = Array.isArray(text) ? text[0] : text;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<CheckIcon className="size-4 shrink-0 text-green-500" />
|
||||
{Array.isArray(text) ? (
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { SelectWithSearch } from "./select-with-search";
|
||||
|
||||
const meta = {
|
||||
@@ -16,17 +16,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function createList(len: number) {
|
||||
return Array.from({ length: len }, (_, i) => ({
|
||||
value: `${i}`,
|
||||
@@ -36,7 +29,7 @@ function createList(len: number) {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
|
||||
<div className="container flex max-w-6xl flex-col gap-6 py-10">
|
||||
<VariantTest storyLabel="5 items" listLen={5} />
|
||||
<VariantTest storyLabel="5000 items" listLen={5000} />
|
||||
<VariantTest
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { Check, Copy } from "lucide-react";
|
||||
import { Check, Copy, XIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { type ThirdwebClient, isAddress } from "thirdweb";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
@@ -16,6 +16,7 @@ import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
import { ToolTipLabel } from "../ui/tooltip";
|
||||
import { Img } from "./Img";
|
||||
|
||||
export function WalletAddress(props: {
|
||||
@@ -44,7 +45,16 @@ export function WalletAddress(props: {
|
||||
const { onCopy, hasCopied } = useClipboard(address, 2000);
|
||||
|
||||
if (!isAddress(address)) {
|
||||
return <span>Invalid Address ({address})</span>;
|
||||
return (
|
||||
<ToolTipLabel label={address} hoverable>
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
|
||||
<div className="flex size-6 items-center justify-center rounded-full border bg-background">
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
Invalid Address
|
||||
</span>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
}
|
||||
|
||||
// special case for zero address
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../stories/utils";
|
||||
import { BadgeContainer } from "../../stories/utils";
|
||||
import { PaginationButtons } from "./pagination-buttons";
|
||||
|
||||
const meta = {
|
||||
@@ -16,17 +16,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
|
||||
|
||||
@@ -91,13 +91,11 @@ export function DatePickerWithRange(props: {
|
||||
name: "From",
|
||||
onClick: () => setScreen("from"),
|
||||
isActive: screen === "from",
|
||||
isEnabled: true,
|
||||
},
|
||||
{
|
||||
name: "To",
|
||||
onClick: () => setScreen("to"),
|
||||
isActive: screen === "to",
|
||||
isEnabled: true,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -15,6 +15,7 @@ export type NavButtonProps = {
|
||||
action: string;
|
||||
label: string;
|
||||
};
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
|
||||
@@ -30,7 +31,9 @@ export function NavLink(props: React.PropsWithChildren<NavButtonProps>) {
|
||||
href={props.href}
|
||||
className={cn(props.className, isActive && props.activeClassName)}
|
||||
target={props.href.startsWith("http") ? "_blank" : undefined}
|
||||
prefetch={false}
|
||||
onClick={() => {
|
||||
props.onClick?.();
|
||||
if (props.tracking) {
|
||||
track({
|
||||
category: props.tracking.category,
|
||||
|
||||
@@ -96,7 +96,10 @@ const BreadcrumbEllipsis = ({
|
||||
<span
|
||||
role="presentation"
|
||||
aria-hidden="true"
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
|
||||
@@ -6,9 +6,6 @@ import { BadgeContainer } from "../../../stories/utils";
|
||||
const meta = {
|
||||
title: "Shadcn/Buttons",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { CodeClient } from "./code.client";
|
||||
|
||||
const meta = {
|
||||
@@ -11,17 +11,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const tsCode = `\
|
||||
type User = {
|
||||
name: string;
|
||||
@@ -86,7 +79,7 @@ const bashCode = "pnpm i thirdweb";
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<BadgeContainer label="ts">
|
||||
<CodeClient code={tsCode} lang="ts" />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import * as parserBabel from "prettier/plugins/babel";
|
||||
import * as estree from "prettier/plugins/estree";
|
||||
import { format } from "prettier/standalone";
|
||||
import { type BundledLanguage, codeToHtml } from "shiki";
|
||||
|
||||
@@ -21,10 +20,11 @@ export async function getCodeHtml(
|
||||
ignoreFormattingErrors?: boolean;
|
||||
},
|
||||
) {
|
||||
const estreePlugin = await import("prettier/plugins/estree");
|
||||
const formattedCode = isPrettierSupportedLang(lang)
|
||||
? await format(code, {
|
||||
parser: "babel-ts",
|
||||
plugins: [parserBabel, estree],
|
||||
plugins: [parserBabel, estreePlugin.default],
|
||||
printWidth: 60,
|
||||
}).catch((e) => {
|
||||
if (!options?.ignoreFormattingErrors) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { BadgeContainer } from "stories/utils";
|
||||
import { PlainTextCodeBlock } from "./plaintext-code";
|
||||
|
||||
const meta = {
|
||||
@@ -11,17 +11,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const jsCode = `\
|
||||
import { getContract } from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
@@ -39,7 +32,7 @@ const overflowText =
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<BadgeContainer label="large chunk of code">
|
||||
<PlainTextCodeBlock code={jsCode} />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background selection:bg-foreground/10 file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground placeholder:text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background selection:bg-foreground/10 file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground placeholder:text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:selection:bg-foreground/20",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -94,7 +94,10 @@ const PaginationEllipsis = ({
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden
|
||||
className={cn("flex h-9 w-9 items-center justify-center", className)}
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
|
||||
@@ -13,9 +13,6 @@ import { BadgeContainer } from "../../../stories/utils";
|
||||
const meta = {
|
||||
title: "Shadcn/Select",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -0,0 +1,773 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { type VariantProps, cva } from "class-variance-authority";
|
||||
import { PanelLeft } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
const SIDEBAR_COOKIE_NAME = "sidebar_state";
|
||||
const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7;
|
||||
const SIDEBAR_WIDTH = "16rem";
|
||||
const SIDEBAR_WIDTH_MOBILE = "18rem";
|
||||
const SIDEBAR_WIDTH_ICON = "3rem";
|
||||
const SIDEBAR_KEYBOARD_SHORTCUT = "b";
|
||||
|
||||
type SidebarContextProps = {
|
||||
state: "expanded" | "collapsed";
|
||||
open: boolean;
|
||||
setOpen: (open: boolean) => void;
|
||||
openMobile: boolean;
|
||||
setOpenMobile: (open: boolean) => void;
|
||||
isMobile: boolean;
|
||||
toggleSidebar: () => void;
|
||||
};
|
||||
|
||||
const SidebarContext = React.createContext<SidebarContextProps | null>(null);
|
||||
|
||||
function useSidebar() {
|
||||
const context = React.useContext(SidebarContext);
|
||||
if (!context) {
|
||||
throw new Error("useSidebar must be used within a SidebarProvider.");
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
const SidebarProvider = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
defaultOpen?: boolean;
|
||||
open?: boolean;
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
defaultOpen = true,
|
||||
open: openProp,
|
||||
onOpenChange: setOpenProp,
|
||||
className,
|
||||
style,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const isMobile = useIsMobile();
|
||||
const [openMobile, setOpenMobile] = React.useState(false);
|
||||
|
||||
// This is the internal state of the sidebar.
|
||||
// We use openProp and setOpenProp for control from outside the component.
|
||||
const [_open, _setOpen] = React.useState(defaultOpen);
|
||||
const open = openProp ?? _open;
|
||||
const setOpen = React.useCallback(
|
||||
(value: boolean | ((value: boolean) => boolean)) => {
|
||||
const openState = typeof value === "function" ? value(open) : value;
|
||||
if (setOpenProp) {
|
||||
setOpenProp(openState);
|
||||
} else {
|
||||
_setOpen(openState);
|
||||
}
|
||||
|
||||
// This sets the cookie to keep the sidebar state.
|
||||
// eslint-disable-next-line react-compiler/react-compiler
|
||||
document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
|
||||
},
|
||||
[setOpenProp, open],
|
||||
);
|
||||
|
||||
// Helper to toggle the sidebar.
|
||||
const toggleSidebar = React.useCallback(() => {
|
||||
return isMobile
|
||||
? setOpenMobile((open) => !open)
|
||||
: setOpen((open) => !open);
|
||||
}, [isMobile, setOpen]);
|
||||
|
||||
// Adds a keyboard shortcut to toggle the sidebar.
|
||||
React.useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (
|
||||
event.key === SIDEBAR_KEYBOARD_SHORTCUT &&
|
||||
(event.metaKey || event.ctrlKey)
|
||||
) {
|
||||
event.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("keydown", handleKeyDown);
|
||||
return () => window.removeEventListener("keydown", handleKeyDown);
|
||||
}, [toggleSidebar]);
|
||||
|
||||
// We add a state so that we can do data-state="expanded" or "collapsed".
|
||||
// This makes it easier to style the sidebar with Tailwind classes.
|
||||
const state = open ? "expanded" : "collapsed";
|
||||
|
||||
const contextValue = React.useMemo<SidebarContextProps>(
|
||||
() => ({
|
||||
state,
|
||||
open,
|
||||
setOpen,
|
||||
isMobile,
|
||||
openMobile,
|
||||
setOpenMobile,
|
||||
toggleSidebar,
|
||||
}),
|
||||
[state, open, setOpen, isMobile, openMobile, toggleSidebar],
|
||||
);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH,
|
||||
"--sidebar-width-icon": SIDEBAR_WIDTH_ICON,
|
||||
...style,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={cn(
|
||||
"group/sidebar-wrapper flex min-h-svh w-full has-[[data-variant=inset]]:bg-sidebar",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</TooltipProvider>
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarProvider.displayName = "SidebarProvider";
|
||||
|
||||
const Sidebar = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
side?: "left" | "right";
|
||||
variant?: "sidebar" | "floating" | "inset";
|
||||
collapsible?: "offcanvas" | "icon" | "none";
|
||||
}
|
||||
>(
|
||||
(
|
||||
{
|
||||
side = "left",
|
||||
variant = "sidebar",
|
||||
collapsible = "offcanvas",
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const { isMobile, state, openMobile, setOpenMobile } = useSidebar();
|
||||
|
||||
if (collapsible === "none") {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full w-[--sidebar-width] flex-col bg-sidebar text-sidebar-foreground",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Sheet open={openMobile} onOpenChange={setOpenMobile} {...props}>
|
||||
<SheetContent
|
||||
data-sidebar="sidebar"
|
||||
data-mobile="true"
|
||||
className="w-[--sidebar-width] bg-sidebar p-0 text-sidebar-foreground [&>button]:hidden"
|
||||
style={
|
||||
{
|
||||
"--sidebar-width": SIDEBAR_WIDTH_MOBILE,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
side={side}
|
||||
>
|
||||
<SheetHeader className="sr-only">
|
||||
<SheetTitle>Sidebar</SheetTitle>
|
||||
<SheetDescription>Displays the mobile sidebar.</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex h-full w-full flex-col">{children}</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="group peer relative hidden flex-shrink-0 text-sidebar-foreground md:block"
|
||||
data-state={state}
|
||||
data-collapsible={state === "collapsed" ? collapsible : ""}
|
||||
data-variant={variant}
|
||||
data-side={side}
|
||||
>
|
||||
{/* This is what handles the sidebar gap on desktop */}
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-[--sidebar-width] bg-transparent transition-[width] duration-200 ease-linear",
|
||||
"group-data-[collapsible=offcanvas]:w-0",
|
||||
"group-data-[side=right]:rotate-180",
|
||||
variant === "floating" || variant === "inset"
|
||||
? "group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4))]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon]",
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute inset-y-0 z-10 hidden h-full w-[--sidebar-width] transition-[left,right,width] duration-200 ease-linear md:flex",
|
||||
side === "left"
|
||||
? "left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]"
|
||||
: "right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]",
|
||||
// Adjust the padding for floating and inset variants.
|
||||
variant === "floating" || variant === "inset"
|
||||
? "p-2 group-data-[collapsible=icon]:w-[calc(var(--sidebar-width-icon)_+_theme(spacing.4)_+2px)]"
|
||||
: "group-data-[collapsible=icon]:w-[--sidebar-width-icon] group-data-[side=left]:border-r group-data-[side=right]:border-l",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
data-sidebar="sidebar"
|
||||
className="flex h-full w-full flex-col bg-sidebar group-data-[variant=floating]:rounded-lg group-data-[variant=floating]:border group-data-[variant=floating]:border-sidebar-border group-data-[variant=floating]:shadow"
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
Sidebar.displayName = "Sidebar";
|
||||
|
||||
const SidebarTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof Button>,
|
||||
React.ComponentProps<typeof Button>
|
||||
>(({ className, onClick, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
data-sidebar="trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className={cn("h-7 w-7", className)}
|
||||
onClick={(event) => {
|
||||
onClick?.(event);
|
||||
toggleSidebar();
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
<PanelLeft />
|
||||
<span className="sr-only">Toggle Sidebar</span>
|
||||
</Button>
|
||||
);
|
||||
});
|
||||
SidebarTrigger.displayName = "SidebarTrigger";
|
||||
|
||||
const SidebarRail = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button">
|
||||
>(({ className, ...props }, ref) => {
|
||||
const { toggleSidebar } = useSidebar();
|
||||
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
data-sidebar="rail"
|
||||
aria-label="Toggle Sidebar"
|
||||
tabIndex={-1}
|
||||
onClick={toggleSidebar}
|
||||
title="Toggle Sidebar"
|
||||
className={cn(
|
||||
"-translate-x-1/2 group-data-[side=left]:-right-4 absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=right]:left-0 sm:flex",
|
||||
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
||||
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
||||
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:hover:bg-sidebar group-data-[collapsible=offcanvas]:after:left-full",
|
||||
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
||||
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarRail.displayName = "SidebarRail";
|
||||
|
||||
const SidebarInset = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"main">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<main
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full flex-1 flex-col bg-background",
|
||||
"md:peer-data-[state=collapsed]:peer-data-[variant=inset]:ml-2 md:peer-data-[variant=inset]:m-2 md:peer-data-[variant=inset]:ml-0 md:peer-data-[variant=inset]:rounded-xl md:peer-data-[variant=inset]:shadow",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInset.displayName = "SidebarInset";
|
||||
|
||||
const SidebarInput = React.forwardRef<
|
||||
React.ElementRef<typeof Input>,
|
||||
React.ComponentProps<typeof Input>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Input
|
||||
ref={ref}
|
||||
data-sidebar="input"
|
||||
className={cn(
|
||||
"h-8 w-full bg-background shadow-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarInput.displayName = "SidebarInput";
|
||||
|
||||
const SidebarHeader = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="header"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarHeader.displayName = "SidebarHeader";
|
||||
|
||||
const SidebarFooter = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="footer"
|
||||
className={cn("flex flex-col gap-2 p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarFooter.displayName = "SidebarFooter";
|
||||
|
||||
const SidebarSeparator = React.forwardRef<
|
||||
React.ElementRef<typeof Separator>,
|
||||
React.ComponentProps<typeof Separator>
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<Separator
|
||||
ref={ref}
|
||||
data-sidebar="separator"
|
||||
className={cn("mx-2 w-auto bg-sidebar-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarSeparator.displayName = "SidebarSeparator";
|
||||
|
||||
const SidebarContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="content"
|
||||
className={cn(
|
||||
"flex min-h-0 flex-1 flex-col gap-2 overflow-auto group-data-[collapsible=icon]:overflow-hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarContent.displayName = "SidebarContent";
|
||||
|
||||
const SidebarGroup = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group"
|
||||
className={cn("relative flex w-full min-w-0 flex-col p-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroup.displayName = "SidebarGroup";
|
||||
|
||||
const SidebarGroupLabel = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-label"
|
||||
className={cn(
|
||||
"flex h-8 shrink-0 items-center rounded-md px-2 font-medium text-sidebar-foreground/70 text-xs outline-none ring-sidebar-ring transition-[margin,opacity] duration-200 ease-linear focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
"group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupLabel.displayName = "SidebarGroupLabel";
|
||||
|
||||
const SidebarGroupAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & { asChild?: boolean }
|
||||
>(({ className, asChild = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="group-action"
|
||||
className={cn(
|
||||
"absolute top-3.5 right-3 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:-inset-2 after:absolute after:md:hidden",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarGroupAction.displayName = "SidebarGroupAction";
|
||||
|
||||
const SidebarGroupContent = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="group-content"
|
||||
className={cn("w-full text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarGroupContent.displayName = "SidebarGroupContent";
|
||||
|
||||
const SidebarMenu = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu"
|
||||
className={cn("flex w-full min-w-0 flex-col gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenu.displayName = "SidebarMenu";
|
||||
|
||||
const SidebarMenuItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
ref={ref}
|
||||
data-sidebar="menu-item"
|
||||
className={cn("group/menu-item relative", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuItem.displayName = "SidebarMenuItem";
|
||||
|
||||
const sidebarMenuButtonVariants = cva(
|
||||
"peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!p-2 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
|
||||
outline:
|
||||
"bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:shadow-[0_0_0_1px_hsl(var(--sidebar-accent))]",
|
||||
},
|
||||
size: {
|
||||
default: "h-8 text-sm",
|
||||
sm: "h-7 text-xs",
|
||||
lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const SidebarMenuButton = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
isActive?: boolean;
|
||||
tooltip?: string | React.ComponentProps<typeof TooltipContent>;
|
||||
} & VariantProps<typeof sidebarMenuButtonVariants>
|
||||
>(
|
||||
(
|
||||
{
|
||||
asChild = false,
|
||||
isActive = false,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
tooltip,
|
||||
className,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
const { isMobile, state } = useSidebar();
|
||||
|
||||
const button = (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!tooltip) {
|
||||
return button;
|
||||
}
|
||||
|
||||
if (typeof tooltip === "string") {
|
||||
tooltip = {
|
||||
children: tooltip,
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side="right"
|
||||
align="center"
|
||||
hidden={state !== "collapsed" || isMobile}
|
||||
{...tooltip}
|
||||
/>
|
||||
</Tooltip>
|
||||
);
|
||||
},
|
||||
);
|
||||
SidebarMenuButton.displayName = "SidebarMenuButton";
|
||||
|
||||
const SidebarMenuAction = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
React.ComponentProps<"button"> & {
|
||||
asChild?: boolean;
|
||||
showOnHover?: boolean;
|
||||
}
|
||||
>(({ className, asChild = false, showOnHover = false, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-action"
|
||||
className={cn(
|
||||
"absolute top-1.5 right-1 flex aspect-square w-5 items-center justify-center rounded-md p-0 text-sidebar-foreground outline-none ring-sidebar-ring transition-transform hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 peer-hover/menu-button:text-sidebar-accent-foreground [&>svg]:size-4 [&>svg]:shrink-0",
|
||||
// Increases the hit area of the button on mobile.
|
||||
"after:-inset-2 after:absolute after:md:hidden",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
showOnHover &&
|
||||
"group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 peer-data-[active=true]/menu-button:text-sidebar-accent-foreground md:opacity-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuAction.displayName = "SidebarMenuAction";
|
||||
|
||||
const SidebarMenuBadge = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-badge"
|
||||
className={cn(
|
||||
"pointer-events-none absolute right-1 flex h-5 min-w-5 select-none items-center justify-center rounded-md px-1 font-medium text-sidebar-foreground text-xs tabular-nums",
|
||||
"peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[active=true]/menu-button:text-sidebar-accent-foreground",
|
||||
"peer-data-[size=sm]/menu-button:top-1",
|
||||
"peer-data-[size=default]/menu-button:top-1.5",
|
||||
"peer-data-[size=lg]/menu-button:top-2.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuBadge.displayName = "SidebarMenuBadge";
|
||||
|
||||
const SidebarMenuSkeleton = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
React.ComponentProps<"div"> & {
|
||||
showIcon?: boolean;
|
||||
}
|
||||
>(({ className, showIcon = false, ...props }, ref) => {
|
||||
// Random width between 50 to 90%.
|
||||
const width = React.useMemo(() => {
|
||||
return `${Math.floor(Math.random() * 40) + 50}%`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
data-sidebar="menu-skeleton"
|
||||
className={cn("flex h-8 items-center gap-2 rounded-md px-2", className)}
|
||||
{...props}
|
||||
>
|
||||
{showIcon && (
|
||||
<Skeleton
|
||||
className="size-4 rounded-md"
|
||||
data-sidebar="menu-skeleton-icon"
|
||||
/>
|
||||
)}
|
||||
<Skeleton
|
||||
className="h-4 max-w-[--skeleton-width] flex-1"
|
||||
data-sidebar="menu-skeleton-text"
|
||||
style={
|
||||
{
|
||||
"--skeleton-width": width,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
SidebarMenuSkeleton.displayName = "SidebarMenuSkeleton";
|
||||
|
||||
const SidebarMenuSub = React.forwardRef<
|
||||
HTMLUListElement,
|
||||
React.ComponentProps<"ul">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ul
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub"
|
||||
className={cn(
|
||||
"mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-sidebar-border border-l px-2.5 py-0.5",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
SidebarMenuSub.displayName = "SidebarMenuSub";
|
||||
|
||||
const SidebarMenuSubItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentProps<"li">
|
||||
>(({ ...props }, ref) => <li ref={ref} {...props} />);
|
||||
SidebarMenuSubItem.displayName = "SidebarMenuSubItem";
|
||||
|
||||
const SidebarMenuSubButton = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentProps<"a"> & {
|
||||
asChild?: boolean;
|
||||
size?: "sm" | "md";
|
||||
isActive?: boolean;
|
||||
}
|
||||
>(({ asChild = false, size = "md", isActive, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
data-sidebar="menu-sub-button"
|
||||
data-size={size}
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"-translate-x-px flex h-7 min-w-0 items-center gap-2 overflow-hidden rounded-md px-2 text-sidebar-foreground outline-none ring-sidebar-ring hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground",
|
||||
"data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground",
|
||||
size === "sm" && "text-xs",
|
||||
size === "md" && "text-sm",
|
||||
"group-data-[collapsible=icon]:hidden",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
SidebarMenuSubButton.displayName = "SidebarMenuSubButton";
|
||||
|
||||
export {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
SidebarFooter,
|
||||
SidebarGroup,
|
||||
SidebarGroupAction,
|
||||
SidebarGroupContent,
|
||||
SidebarGroupLabel,
|
||||
SidebarHeader,
|
||||
SidebarInput,
|
||||
SidebarInset,
|
||||
SidebarMenu,
|
||||
SidebarMenuAction,
|
||||
SidebarMenuBadge,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
SidebarMenuSkeleton,
|
||||
SidebarMenuSub,
|
||||
SidebarMenuSubButton,
|
||||
SidebarMenuSubItem,
|
||||
SidebarProvider,
|
||||
SidebarRail,
|
||||
SidebarSeparator,
|
||||
SidebarTrigger,
|
||||
useSidebar,
|
||||
};
|
||||
@@ -10,35 +10,25 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import Link from "next/link";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Badge } from "./badge";
|
||||
|
||||
const meta = {
|
||||
title: "Shadcn/Table",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Variants: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="flex min-w-0 flex-col gap-6 px-4 py-6 lg:mx-auto lg:max-w-[1000px]">
|
||||
<div className="container flex max-w-6xl flex-col gap-10 py-10">
|
||||
<BadgeContainer label="Normal">
|
||||
<TableDemo />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { usePathname } from "next/navigation";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
|
||||
import { Button } from "./button";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
export type TabLink = {
|
||||
name: string;
|
||||
@@ -20,6 +21,7 @@ export function TabLinks(props: {
|
||||
className?: string;
|
||||
tabContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
scrollableClassName?: string;
|
||||
}) {
|
||||
const { containerRef, lineRef, activeTabRef } =
|
||||
useUnderline<HTMLAnchorElement>();
|
||||
@@ -30,7 +32,7 @@ export function TabLinks(props: {
|
||||
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
|
||||
|
||||
<ScrollShadow
|
||||
scrollableClassName="pb-[8px] relative"
|
||||
scrollableClassName={cn("pb-[8px] relative", props.scrollableClassName)}
|
||||
shadowColor={props.shadowColor}
|
||||
>
|
||||
<div
|
||||
@@ -79,8 +81,9 @@ export function TabButtons(props: {
|
||||
name: React.ReactNode;
|
||||
onClick: () => void;
|
||||
isActive: boolean;
|
||||
isEnabled?: boolean;
|
||||
isDisabled?: boolean;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
toolTip?: string;
|
||||
}[];
|
||||
tabClassName?: string;
|
||||
activeTabClassName?: string;
|
||||
@@ -110,26 +113,31 @@ export function TabButtons(props: {
|
||||
>
|
||||
{props.tabs.map((tab, index) => {
|
||||
return (
|
||||
<Button
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: tabs don't change order, so index is stable
|
||||
key={index}
|
||||
variant="ghost"
|
||||
ref={tab.isActive ? activeTabRef : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex h-auto items-center gap-1.5 rounded-lg px-2 font-medium text-sm hover:bg-accent lg:px-3 lg:text-base",
|
||||
!tab.isActive &&
|
||||
"text-muted-foreground hover:text-foreground",
|
||||
!tab.isEnabled && "cursor-not-allowed opacity-50",
|
||||
props.tabClassName,
|
||||
tab.isActive && props.activeTabClassName,
|
||||
)}
|
||||
onClick={tab.isEnabled ? tab.onClick : undefined}
|
||||
<ToolTipLabel
|
||||
key={typeof tab.name === "string" ? tab.name : index}
|
||||
label={tab.toolTip}
|
||||
>
|
||||
{tab.icon && (
|
||||
<tab.icon className={cn("size-6", props.tabIconClassName)} />
|
||||
)}
|
||||
{tab.name}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
ref={tab.isActive ? activeTabRef : undefined}
|
||||
className={cn(
|
||||
"relative inline-flex h-auto items-center gap-1.5 rounded-lg px-2 font-medium text-sm hover:bg-accent lg:px-3 lg:text-base",
|
||||
!tab.isActive &&
|
||||
"text-muted-foreground hover:text-foreground",
|
||||
tab.isDisabled && "cursor-not-allowed opacity-50",
|
||||
props.tabClassName,
|
||||
tab.isActive && props.activeTabClassName,
|
||||
)}
|
||||
onClick={!tab.isDisabled ? tab.onClick : undefined}
|
||||
>
|
||||
{tab.icon && (
|
||||
<tab.icon
|
||||
className={cn("size-6", props.tabIconClassName)}
|
||||
/>
|
||||
)}
|
||||
{tab.name}
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
@@ -159,9 +167,11 @@ function useUnderline<El extends HTMLElement>() {
|
||||
const containerRect = containerRef.current.getBoundingClientRect();
|
||||
const lineEl = lineRef.current;
|
||||
const rect = activeTabEl.getBoundingClientRect();
|
||||
const containerPaddingLeft =
|
||||
containerRect.left - containerRef.current.offsetLeft;
|
||||
lineEl.style.width = `${rect.width}px`;
|
||||
lineEl.style.transform = `translateX(${
|
||||
rect.left - containerRect.left
|
||||
rect.left - containerPaddingLeft
|
||||
}px)`;
|
||||
setTimeout(() => {
|
||||
lineEl.style.transition = "transform 0.3s, width 0.3s";
|
||||
@@ -205,6 +215,7 @@ export function TabPathLinks(props: {
|
||||
className?: string;
|
||||
tabContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
scrollableClassName?: string;
|
||||
}) {
|
||||
const pathname = usePathname() || "";
|
||||
const { links, ...restProps } = props;
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import * as React from "react";
|
||||
|
||||
const MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export function useIsMobile() {
|
||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`);
|
||||
const onChange = () => {
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
};
|
||||
mql.addEventListener("change", onChange);
|
||||
setIsMobile(window.innerWidth < MOBILE_BREAKPOINT);
|
||||
return () => mql.removeEventListener("change", onChange);
|
||||
}, []);
|
||||
|
||||
return !!isMobile;
|
||||
}
|
||||
@@ -3,6 +3,8 @@ export type ProductSKU =
|
||||
| "plan:starter"
|
||||
| "plan:growth"
|
||||
| "plan:custom"
|
||||
| "plan:accelerate"
|
||||
| "plan:scale"
|
||||
| "product:ecosystem_wallets"
|
||||
| "product:engine_standard"
|
||||
| "product:engine_premium"
|
||||
|
||||
@@ -44,14 +44,6 @@ export type Account = {
|
||||
// TODO - add image URL
|
||||
};
|
||||
|
||||
interface UpdateAccountInput {
|
||||
name?: string;
|
||||
email?: string;
|
||||
linkWallet?: boolean;
|
||||
subscribeToUpdates?: boolean;
|
||||
onboardSkipped?: boolean;
|
||||
}
|
||||
|
||||
interface UpdateAccountNotificationsInput {
|
||||
billing: "email" | "none";
|
||||
updates: "email" | "none";
|
||||
@@ -140,45 +132,40 @@ export function useAccountCredits() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateAccount() {
|
||||
const queryClient = useQueryClient();
|
||||
const address = useActiveAccount()?.address;
|
||||
type UpdateAccountParams = {
|
||||
name?: string;
|
||||
email?: string;
|
||||
linkWallet?: boolean;
|
||||
subscribeToUpdates?: boolean;
|
||||
onboardSkipped?: boolean;
|
||||
};
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateAccountInput) => {
|
||||
type Result = {
|
||||
data: object;
|
||||
error?: { message: string };
|
||||
};
|
||||
export async function updateAccountClient(input: UpdateAccountParams) {
|
||||
type Result = {
|
||||
data: object;
|
||||
error?: { message: string };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account",
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
},
|
||||
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(address || ""),
|
||||
});
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account",
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export function useUpdateNotifications() {
|
||||
@@ -221,77 +208,61 @@ export function useUpdateNotifications() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useConfirmEmail() {
|
||||
return useMutation({
|
||||
mutationFn: async (input: ConfirmEmailInput) => {
|
||||
type Result = {
|
||||
error?: { message: string };
|
||||
data: { team: Team; account: Account };
|
||||
};
|
||||
export const verifyEmailClient = async (input: ConfirmEmailInput) => {
|
||||
type Result = {
|
||||
error?: { message: string };
|
||||
data: { team: Team; account: Account };
|
||||
};
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account/confirmEmail",
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account/confirmEmail",
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
}
|
||||
|
||||
export function useResendEmailConfirmation() {
|
||||
const address = useActiveAccount()?.address;
|
||||
const queryClient = useQueryClient();
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
type Result = {
|
||||
error?: { message: string };
|
||||
data: object;
|
||||
};
|
||||
const json = res.data;
|
||||
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account/resendEmailConfirmation",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
return json.data;
|
||||
};
|
||||
|
||||
const json = res.data;
|
||||
export const resendEmailClient = async () => {
|
||||
type Result = {
|
||||
error?: { message: string };
|
||||
data: object;
|
||||
};
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(address || ""),
|
||||
});
|
||||
const res = await apiServerProxy<Result>({
|
||||
pathname: "/v1/account/resendEmailConfirmation",
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
});
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
|
||||
const json = res.data;
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
};
|
||||
|
||||
export async function createProjectClient(
|
||||
teamId: string,
|
||||
|
||||
@@ -15,8 +15,6 @@ import invariant from "tiny-invariant";
|
||||
import type { EngineStatus } from "../../../app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
|
||||
export type EngineTier = "STARTER" | "PREMIUM" | "ENTERPRISE";
|
||||
|
||||
// Engine instances
|
||||
export type EngineInstance = {
|
||||
id: string;
|
||||
@@ -24,6 +22,8 @@ export type EngineInstance = {
|
||||
name: string;
|
||||
url: string;
|
||||
lastAccessedAt: string;
|
||||
isCloudHosted: boolean;
|
||||
isPlanEngine: boolean;
|
||||
status:
|
||||
| "active"
|
||||
| "pending"
|
||||
|
||||
@@ -5,6 +5,7 @@ import invariant from "tiny-invariant";
|
||||
export function useWalletNFTs(params: {
|
||||
chainId: number;
|
||||
walletAddress?: string;
|
||||
isInsightSupported: boolean;
|
||||
}) {
|
||||
return useQuery({
|
||||
queryKey: ["walletNfts", params.chainId, params.walletAddress],
|
||||
@@ -13,6 +14,7 @@ export function useWalletNFTs(params: {
|
||||
return getWalletNFTs({
|
||||
chainId: params.chainId,
|
||||
owner: params.walletAddress,
|
||||
isInsightSupported: params.isInsightSupported,
|
||||
});
|
||||
},
|
||||
enabled: !!params.walletAddress,
|
||||
|
||||
@@ -47,7 +47,7 @@ export const SearchInput: React.FC = () => {
|
||||
<div className="group relative w-full">
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-[50%] left-3 size-4 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search by token address or chain ID"
|
||||
placeholder="Search with anything!"
|
||||
className="h-10 rounded-lg bg-card py-2 pl-9 lg:min-w-[300px]"
|
||||
defaultValue={searchParams?.get("query") || ""}
|
||||
onChange={(e) => handleSearch(e.target.value)}
|
||||
|
||||
+12
-29
@@ -1,59 +1,42 @@
|
||||
import { Card, CardContent, CardHeader } from "@/components/ui/card";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { NATIVE_TOKEN_ADDRESS, defineChain, getContract } from "thirdweb";
|
||||
import { defineChain } from "thirdweb";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { name } from "thirdweb/extensions/common";
|
||||
|
||||
type RouteListCardProps = {
|
||||
originChainId: number;
|
||||
originTokenAddress: string;
|
||||
originTokenIconUri: string | null;
|
||||
originTokenIconUri?: string | null;
|
||||
originTokenSymbol: string;
|
||||
originTokenName: string;
|
||||
destinationChainId: number;
|
||||
destinationTokenAddress: string;
|
||||
destinationTokenIconUri: string | null;
|
||||
destinationTokenIconUri?: string | null;
|
||||
destinationTokenSymbol: string;
|
||||
destinationTokenName: string;
|
||||
};
|
||||
|
||||
export async function RouteListCard({
|
||||
originChainId,
|
||||
originTokenAddress,
|
||||
originTokenIconUri,
|
||||
originTokenName,
|
||||
destinationChainId,
|
||||
destinationTokenAddress,
|
||||
destinationTokenIconUri,
|
||||
destinationTokenName,
|
||||
}: RouteListCardProps) {
|
||||
const [
|
||||
originChain,
|
||||
originTokenName,
|
||||
destinationChain,
|
||||
destinationTokenName,
|
||||
resolvedOriginTokenIconUri,
|
||||
resolvedDestinationTokenIconUri,
|
||||
] = await Promise.all([
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
getChainMetadata(defineChain(originChainId)),
|
||||
originTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
|
||||
? "ETH"
|
||||
: name({
|
||||
contract: getContract({
|
||||
address: originTokenAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(originChainId),
|
||||
client: getThirdwebClient(),
|
||||
}),
|
||||
}).catch(() => undefined),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
getChainMetadata(defineChain(destinationChainId)),
|
||||
destinationTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
|
||||
? "ETH"
|
||||
: name({
|
||||
contract: getContract({
|
||||
address: destinationTokenAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(destinationChainId),
|
||||
client: getThirdwebClient(),
|
||||
}),
|
||||
}).catch(() => undefined),
|
||||
originTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: originTokenIconUri,
|
||||
@@ -78,17 +61,17 @@ export async function RouteListCard({
|
||||
<img
|
||||
src={resolvedOriginTokenIconUri}
|
||||
alt={originTokenAddress}
|
||||
className="size-8 rounded-full bg-white"
|
||||
className="size-8 rounded-full border border-muted-foreground"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-8 rounded-full bg-white/10" />
|
||||
<div className="size-8 rounded-full bg-muted-foreground" />
|
||||
)}
|
||||
{resolvedDestinationTokenIconUri ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={resolvedDestinationTokenIconUri}
|
||||
alt={destinationTokenAddress}
|
||||
className="-translate-x-4 size-8 rounded-full bg-white ring-2 ring-card"
|
||||
className="-translate-x-4 size-8 rounded-full border border-muted-foreground ring-2 ring-card"
|
||||
/>
|
||||
) : (
|
||||
<div className="-translate-x-4 size-8 rounded-full bg-muted-foreground ring-2 ring-card" />
|
||||
|
||||
+10
-28
@@ -2,59 +2,41 @@ import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { TableCell, TableRow } from "@/components/ui/table";
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { NATIVE_TOKEN_ADDRESS, getContract } from "thirdweb";
|
||||
import { defineChain, getChainMetadata } from "thirdweb/chains";
|
||||
import { symbol } from "thirdweb/extensions/common";
|
||||
|
||||
type RouteListRowProps = {
|
||||
originChainId: number;
|
||||
originTokenAddress: string;
|
||||
originTokenIconUri: string | null;
|
||||
originTokenIconUri?: string | null;
|
||||
originTokenSymbol?: string;
|
||||
originTokenName?: string;
|
||||
destinationChainId: number;
|
||||
destinationTokenAddress: string;
|
||||
destinationTokenIconUri: string | null;
|
||||
destinationTokenIconUri?: string | null;
|
||||
destinationTokenSymbol?: string;
|
||||
destinationTokenName?: string;
|
||||
};
|
||||
|
||||
export async function RouteListRow({
|
||||
originChainId,
|
||||
originTokenAddress,
|
||||
originTokenIconUri,
|
||||
originTokenSymbol,
|
||||
destinationChainId,
|
||||
destinationTokenAddress,
|
||||
destinationTokenIconUri,
|
||||
destinationTokenSymbol,
|
||||
}: RouteListRowProps) {
|
||||
const [
|
||||
originChain,
|
||||
originTokenSymbol,
|
||||
destinationChain,
|
||||
destinationTokenSymbol,
|
||||
resolvedOriginTokenIconUri,
|
||||
resolvedDestinationTokenIconUri,
|
||||
] = await Promise.all([
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
getChainMetadata(defineChain(originChainId)),
|
||||
originTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
|
||||
? "ETH"
|
||||
: symbol({
|
||||
contract: getContract({
|
||||
address: originTokenAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(originChainId),
|
||||
client: getThirdwebClient(),
|
||||
}),
|
||||
}).catch(() => undefined),
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
getChainMetadata(defineChain(destinationChainId)),
|
||||
destinationTokenAddress.toLowerCase() === NATIVE_TOKEN_ADDRESS
|
||||
? "ETH"
|
||||
: symbol({
|
||||
contract: getContract({
|
||||
address: destinationTokenAddress,
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
chain: defineChain(destinationChainId),
|
||||
client: getThirdwebClient(),
|
||||
}),
|
||||
}).catch(() => undefined),
|
||||
originTokenIconUri
|
||||
? resolveSchemeWithErrorHandler({
|
||||
uri: originTokenIconUri,
|
||||
@@ -80,7 +62,7 @@ export async function RouteListRow({
|
||||
<img
|
||||
src={resolvedOriginTokenIconUri}
|
||||
alt={originTokenAddress}
|
||||
className="size-6 rounded-full bg-muted-foreground"
|
||||
className="size-6 rounded-full border border-muted-foreground"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-6 rounded-full bg-muted-foreground" />
|
||||
@@ -115,7 +97,7 @@ export async function RouteListRow({
|
||||
<img
|
||||
src={resolvedDestinationTokenIconUri}
|
||||
alt={destinationTokenAddress}
|
||||
className="size-6 rounded-full bg-muted-foreground"
|
||||
className="size-6 rounded-full border border-muted-foreground"
|
||||
/>
|
||||
) : (
|
||||
<div className="size-6 rounded-full bg-muted-foreground" />
|
||||
|
||||
+80
-8
@@ -7,6 +7,7 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { Address } from "thirdweb";
|
||||
import { checksumAddress } from "thirdweb/utils";
|
||||
import { getRoutes } from "../../../utils";
|
||||
import { ChainlistPagination } from "../client/pagination";
|
||||
import { RouteListCard } from "../server/routelist-card";
|
||||
@@ -34,28 +35,83 @@ async function getRoutesToRender(params: SearchParams) {
|
||||
originTokenAddress?: Address;
|
||||
destinationChainId?: number;
|
||||
destinationTokenAddress?: Address;
|
||||
originTextQuery?: string;
|
||||
destinationTextQuery?: string;
|
||||
}> = {};
|
||||
|
||||
if (params.type === "origin" || typeof params.type === "undefined") {
|
||||
if (params.query?.startsWith("0x")) {
|
||||
filters.originTokenAddress = params.query as Address;
|
||||
} else if (params.query) {
|
||||
} else if (Number.isInteger(Number(params.query))) {
|
||||
filters.originChainId = Number(params.query);
|
||||
} else if (params.query) {
|
||||
filters.originTextQuery = params.query;
|
||||
}
|
||||
} else if (params.type === "destination") {
|
||||
if (params.query?.startsWith("0x")) {
|
||||
filters.destinationTokenAddress = params.query as Address;
|
||||
} else if (params.query) {
|
||||
} else if (Number.isInteger(Number(params.query))) {
|
||||
filters.destinationChainId = Number(params.query);
|
||||
} else if (params.query) {
|
||||
filters.destinationTextQuery = params.query;
|
||||
}
|
||||
}
|
||||
// Temporary, will update this after the /routes endpoint
|
||||
filters.limit = 10_000;
|
||||
|
||||
const routes = await getRoutes(filters);
|
||||
let routes = await getRoutes({ limit: 100_000 });
|
||||
|
||||
const totalCount = routes.length;
|
||||
|
||||
if (filters.originChainId) {
|
||||
routes = routes.filter(
|
||||
(route) => route.originToken.chainId === filters.originChainId,
|
||||
);
|
||||
}
|
||||
if (filters.originTokenAddress) {
|
||||
const originTokenAddress = filters.originTokenAddress;
|
||||
routes = routes.filter(
|
||||
(route) =>
|
||||
checksumAddress(route.originToken.address) ===
|
||||
checksumAddress(originTokenAddress),
|
||||
);
|
||||
}
|
||||
if (filters.destinationChainId) {
|
||||
routes = routes.filter(
|
||||
(route) => route.destinationToken.chainId === filters.destinationChainId,
|
||||
);
|
||||
}
|
||||
if (filters.destinationTokenAddress) {
|
||||
const destinationTokenAddress = filters.destinationTokenAddress;
|
||||
routes = routes.filter(
|
||||
(route) =>
|
||||
checksumAddress(route.destinationToken.address) ===
|
||||
checksumAddress(destinationTokenAddress),
|
||||
);
|
||||
}
|
||||
|
||||
if (filters.originTextQuery) {
|
||||
const originTextQuery = filters.originTextQuery.toLowerCase();
|
||||
routes = routes.filter((route) => {
|
||||
return (
|
||||
route.originToken.name.toLowerCase().includes(originTextQuery) ||
|
||||
route.originToken.symbol.toLowerCase().includes(originTextQuery)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (filters.destinationTextQuery) {
|
||||
const destinationTextQuery = filters.destinationTextQuery.toLowerCase();
|
||||
routes = routes.filter((route) => {
|
||||
return (
|
||||
route.destinationToken.name
|
||||
.toLowerCase()
|
||||
.includes(destinationTextQuery) ||
|
||||
route.destinationToken.symbol
|
||||
.toLowerCase()
|
||||
.includes(destinationTextQuery)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
routesToRender: routes,
|
||||
totalCount,
|
||||
@@ -107,9 +163,13 @@ export async function RoutesData(props: {
|
||||
originChainId={route.originToken.chainId}
|
||||
originTokenAddress={route.originToken.address}
|
||||
originTokenIconUri={route.originToken.iconUri}
|
||||
originTokenSymbol={route.originToken.symbol}
|
||||
originTokenName={route.originToken.name}
|
||||
destinationChainId={route.destinationToken.chainId}
|
||||
destinationTokenAddress={route.destinationToken.address}
|
||||
destinationTokenIconUri={route.destinationToken.iconUri}
|
||||
destinationTokenSymbol={route.destinationToken.symbol}
|
||||
destinationTokenName={route.destinationToken.name}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
@@ -126,9 +186,13 @@ export async function RoutesData(props: {
|
||||
originChainId={route.originToken.chainId}
|
||||
originTokenAddress={route.originToken.address}
|
||||
originTokenIconUri={route.originToken.iconUri}
|
||||
originTokenSymbol={route.originToken.symbol}
|
||||
originTokenName={route.originToken.name}
|
||||
destinationChainId={route.destinationToken.chainId}
|
||||
destinationTokenAddress={route.destinationToken.address}
|
||||
destinationTokenIconUri={route.destinationToken.iconUri}
|
||||
destinationTokenSymbol={route.destinationToken.symbol}
|
||||
destinationTokenName={route.destinationToken.name}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
@@ -146,13 +210,21 @@ export async function RoutesData(props: {
|
||||
out of{" "}
|
||||
{filteredCount !== totalCount ? (
|
||||
<>
|
||||
<span className="text-accent-foreground">{filteredCount}</span>{" "}
|
||||
<span className="text-accent-foreground">
|
||||
{filteredCount.toLocaleString()}
|
||||
</span>{" "}
|
||||
routes that match filters. (Total:{" "}
|
||||
<span className="text-accent-foreground">{totalCount}</span>)
|
||||
<span className="text-accent-foreground">
|
||||
{totalCount.toLocaleString()}
|
||||
</span>
|
||||
)
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-accent-foreground">{totalCount}</span> routes.
|
||||
<span className="text-accent-foreground">
|
||||
{totalCount.toLocaleString()}
|
||||
</span>{" "}
|
||||
routes.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 13 KiB |
@@ -22,7 +22,7 @@ export const metadata: Metadata = {
|
||||
},
|
||||
};
|
||||
|
||||
export default async function ChainListPage(props: {
|
||||
export default async function RoutesPage(props: {
|
||||
searchParams: Promise<SearchParams>;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
@@ -4,11 +4,15 @@ export type Route = {
|
||||
originToken: {
|
||||
address: Address;
|
||||
chainId: number;
|
||||
iconUri: string;
|
||||
iconUri?: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
};
|
||||
destinationToken: {
|
||||
address: Address;
|
||||
chainId: number;
|
||||
iconUri: string;
|
||||
iconUri?: string;
|
||||
name: string;
|
||||
symbol: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -2,43 +2,18 @@ import "server-only";
|
||||
|
||||
import { BRIDGE_URL, DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/env";
|
||||
import { getAuthToken } from "app/api/lib/getAuthToken";
|
||||
import type { Address } from "thirdweb";
|
||||
import type { Route } from "./types/route";
|
||||
|
||||
export async function getRoutes({
|
||||
limit,
|
||||
offset,
|
||||
originChainId,
|
||||
originTokenAddress,
|
||||
destinationChainId,
|
||||
destinationTokenAddress,
|
||||
}: {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
originChainId?: number;
|
||||
originTokenAddress?: Address;
|
||||
destinationChainId?: number;
|
||||
destinationTokenAddress?: Address;
|
||||
} = {}) {
|
||||
const url = new URL(`${BRIDGE_URL}/v1/routes`);
|
||||
if (limit) {
|
||||
url.searchParams.set("limit", limit.toString());
|
||||
}
|
||||
if (offset) {
|
||||
url.searchParams.set("offset", offset.toString());
|
||||
}
|
||||
if (originChainId) {
|
||||
url.searchParams.set("originChainId", originChainId.toString());
|
||||
}
|
||||
if (originTokenAddress) {
|
||||
url.searchParams.set("originTokenAddress", originTokenAddress);
|
||||
}
|
||||
if (destinationChainId) {
|
||||
url.searchParams.set("destinationChainId", destinationChainId.toString());
|
||||
}
|
||||
if (destinationTokenAddress) {
|
||||
url.searchParams.set("destinationTokenAddress", destinationTokenAddress);
|
||||
}
|
||||
// It's faster to filter client side, doesn't seem to be a big performance boost to paginate/filter server side
|
||||
const token = await getAuthToken();
|
||||
const routesResponse = await fetch(url, {
|
||||
headers: token
|
||||
|
||||
+2
-2
@@ -52,7 +52,7 @@ import {
|
||||
useWalletBalance,
|
||||
} from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
import { isOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired";
|
||||
import { isAccountOnboardingComplete } from "../../../../../../login/onboarding/isOnboardingRequired";
|
||||
|
||||
function formatTime(seconds: number) {
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
@@ -210,7 +210,7 @@ export function FaucetButton({
|
||||
);
|
||||
}
|
||||
|
||||
if (!isOnboardingComplete(twAccount)) {
|
||||
if (!isAccountOnboardingComplete(twAccount)) {
|
||||
return (
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
|
||||
+3
-2
@@ -28,7 +28,7 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
<div className="h-6" />
|
||||
|
||||
<h2 className="px-4 text-center font-semibold text-lg tracking-tight">
|
||||
Buy Funds on {sanitizedChainName} using thirdweb Pay
|
||||
Buy Funds on {sanitizedChainName} using Universal Bridge
|
||||
</h2>
|
||||
|
||||
<div className="h-2" />
|
||||
@@ -50,8 +50,9 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/connect/pay/overview"
|
||||
className="inline-flex items-center gap-1.5 text-muted-foreground text-sm hover:text-foreground"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more about thirdweb Pay
|
||||
Learn more about Universal Bridge
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+6
-5
@@ -13,7 +13,6 @@ import { ListerOnly } from "@3rdweb-sdk/react/components/roles/lister-only";
|
||||
import type { Account } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { isAlchemySupported } from "lib/wallet/nfts/alchemy";
|
||||
import { isMoralisSupported } from "lib/wallet/nfts/moralis";
|
||||
import { useSimplehashSupport } from "lib/wallet/nfts/simpleHash";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
@@ -25,6 +24,7 @@ interface CreateListingButtonProps {
|
||||
createText?: string;
|
||||
type?: "direct-listings" | "english-auctions";
|
||||
twAccount: Account | undefined;
|
||||
isInsightSupported: boolean;
|
||||
}
|
||||
|
||||
const LISTING_MODES = ["Select NFT", "Manual"] as const;
|
||||
@@ -34,6 +34,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
type,
|
||||
contract,
|
||||
twAccount,
|
||||
isInsightSupported,
|
||||
...restButtonProps
|
||||
}) => {
|
||||
const address = useActiveAccount()?.address;
|
||||
@@ -41,13 +42,12 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
const [listingMode, setListingMode] =
|
||||
useState<(typeof LISTING_MODES)[number]>("Select NFT");
|
||||
|
||||
const simplehashQuery = useSimplehashSupport(contract.chain.id);
|
||||
|
||||
const isSupportedChain =
|
||||
contract.chain.id &&
|
||||
(simplehashQuery.data ||
|
||||
(isInsightSupported ||
|
||||
isAlchemySupported(contract.chain.id) ||
|
||||
isMoralisSupported(contract.chain.id));
|
||||
|
||||
return (
|
||||
<ListerOnly contract={contract}>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
@@ -71,7 +71,6 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
name: mode,
|
||||
isActive: mode === listingMode,
|
||||
onClick: () => setListingMode(mode),
|
||||
isEnabled: true,
|
||||
}))}
|
||||
tabClassName="text-sm gap-2 !text-sm"
|
||||
tabContainerClassName="gap-0.5"
|
||||
@@ -84,6 +83,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
actionText={createText}
|
||||
setOpen={setOpen}
|
||||
mode={listingMode === "Select NFT" ? "automatic" : "manual"}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
@@ -96,6 +96,7 @@ export const CreateListingButton: React.FC<CreateListingButtonProps> = ({
|
||||
actionText={createText}
|
||||
setOpen={setOpen}
|
||||
mode="manual"
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+6
-3
@@ -21,7 +21,6 @@ import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { isAlchemySupported } from "lib/wallet/nfts/alchemy";
|
||||
import { isMoralisSupported } from "lib/wallet/nfts/moralis";
|
||||
import { useSimplehashSupport } from "lib/wallet/nfts/simpleHash";
|
||||
import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
import { CircleAlertIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
@@ -87,6 +86,7 @@ type CreateListingsFormProps = {
|
||||
mode: "automatic" | "manual";
|
||||
type?: "direct-listings" | "english-auctions";
|
||||
twAccount: Account | undefined;
|
||||
isInsightSupported: boolean;
|
||||
};
|
||||
|
||||
const auctionTimes = [
|
||||
@@ -106,16 +106,17 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
setOpen,
|
||||
twAccount,
|
||||
mode,
|
||||
isInsightSupported,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
const chainId = contract.chain.id;
|
||||
const { idToChain } = useAllChainsData();
|
||||
const network = idToChain.get(chainId);
|
||||
const [isFormLoading, setIsFormLoading] = useState(false);
|
||||
const simplehashQuery = useSimplehashSupport(contract.chain.id);
|
||||
|
||||
const isSupportedChain =
|
||||
chainId &&
|
||||
(!!simplehashQuery.data ||
|
||||
(isInsightSupported ||
|
||||
isAlchemySupported(chainId) ||
|
||||
isMoralisSupported(chainId));
|
||||
|
||||
@@ -124,7 +125,9 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
const { data: walletNFTs, isPending: isWalletNFTsLoading } = useWalletNFTs({
|
||||
chainId,
|
||||
walletAddress: account?.address,
|
||||
isInsightSupported,
|
||||
});
|
||||
|
||||
const sendAndConfirmTx = useSendAndConfirmTransaction();
|
||||
const listingNotifications = useTxNotifications(
|
||||
"NFT listed Successfully",
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ export function ContractDirectListingsPageClient(props: {
|
||||
<ContractDirectListingsPage
|
||||
contract={props.contract}
|
||||
twAccount={props.twAccount}
|
||||
isInsightSupported={metadataQuery.data.isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-2
@@ -8,17 +8,21 @@ import { DirectListingsTable } from "./components/table";
|
||||
interface ContractDirectListingsPageProps {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
isInsightSupported: boolean;
|
||||
}
|
||||
|
||||
export const ContractDirectListingsPage: React.FC<
|
||||
ContractDirectListingsPageProps
|
||||
> = ({ contract, twAccount }) => {
|
||||
> = ({ contract, twAccount, isInsightSupported }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<p className="text-lg">Contract Listings</p>
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
Direct Listings
|
||||
</h2>
|
||||
<div className="flex flex-row gap-4">
|
||||
<CreateListingButton
|
||||
isInsightSupported={isInsightSupported}
|
||||
contract={contract}
|
||||
type="direct-listings"
|
||||
createText="Create Direct Listing"
|
||||
|
||||
+7
-4
@@ -29,15 +29,18 @@ export default async function Page(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const { isDirectListingSupported } = await getContractPageMetadata(
|
||||
info.contract,
|
||||
);
|
||||
const { isDirectListingSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.contract);
|
||||
|
||||
if (!isDirectListingSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<ContractDirectListingsPage contract={info.contract} twAccount={account} />
|
||||
<ContractDirectListingsPage
|
||||
contract={info.contract}
|
||||
twAccount={account}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+1
@@ -29,6 +29,7 @@ export function ContractEnglishAuctionsPageClient(props: {
|
||||
<ContractEnglishAuctionsPage
|
||||
contract={props.contract}
|
||||
twAccount={props.twAccount}
|
||||
isInsightSupported={metadataQuery.data.isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+6
-2
@@ -8,21 +8,25 @@ import { EnglishAuctionsTable } from "./components/table";
|
||||
interface ContractEnglishAuctionsProps {
|
||||
contract: ThirdwebContract;
|
||||
twAccount: Account | undefined;
|
||||
isInsightSupported: boolean;
|
||||
}
|
||||
|
||||
export const ContractEnglishAuctionsPage: React.FC<
|
||||
ContractEnglishAuctionsProps
|
||||
> = ({ contract, twAccount }) => {
|
||||
> = ({ contract, twAccount, isInsightSupported }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<p className="text-lg">Contract Auctions</p>
|
||||
<h2 className="font-semibold text-2xl tracking-tight">
|
||||
English Auctions
|
||||
</h2>
|
||||
<div className="flex flex-row gap-4">
|
||||
<CreateListingButton
|
||||
contract={contract}
|
||||
type="english-auctions"
|
||||
createText="Create English Auction"
|
||||
twAccount={twAccount}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-3
@@ -30,9 +30,8 @@ export default async function Page(props: {
|
||||
);
|
||||
}
|
||||
|
||||
const { isEnglishAuctionSupported } = await getContractPageMetadata(
|
||||
info.contract,
|
||||
);
|
||||
const { isEnglishAuctionSupported, isInsightSupported } =
|
||||
await getContractPageMetadata(info.contract);
|
||||
|
||||
if (!isEnglishAuctionSupported) {
|
||||
redirect(`/${params.chain_id}/${params.contractAddress}`);
|
||||
@@ -42,6 +41,7 @@ export default async function Page(props: {
|
||||
<ContractEnglishAuctionsPage
|
||||
contract={info.contract}
|
||||
twAccount={twAccount}
|
||||
isInsightSupported={isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+69
-54
@@ -1,14 +1,7 @@
|
||||
import {
|
||||
Flex,
|
||||
GridItem,
|
||||
SimpleGrid,
|
||||
Skeleton,
|
||||
SkeletonText,
|
||||
useBreakpointValue,
|
||||
} from "@chakra-ui/react";
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { useMemo } from "react";
|
||||
import { type NFT, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Card, TrackedLink, type TrackedLinkProps } from "tw-components";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
|
||||
type NFTWithContract = NFT & { contractAddress: string; chainId: number };
|
||||
@@ -20,7 +13,8 @@ const dummyMetadata: (idx: number) => NFTWithContract = (idx) => ({
|
||||
tokenURI: `1-0x123-${idx}`,
|
||||
metadata: {
|
||||
name: "Loading...",
|
||||
description: "lorem ipsum loading sit amet",
|
||||
description:
|
||||
"lorem ipsum loading sit amet consectetur adipisicing elit. Quisquam, quos.",
|
||||
id: BigInt(idx || 0),
|
||||
uri: `1-0x123-${idx}`,
|
||||
},
|
||||
@@ -31,7 +25,7 @@ const dummyMetadata: (idx: number) => NFTWithContract = (idx) => ({
|
||||
|
||||
interface NFTCardsProps {
|
||||
nfts: Array<NFTWithContract>;
|
||||
trackingCategory: TrackedLinkProps["category"];
|
||||
trackingCategory: string;
|
||||
isPending: boolean;
|
||||
allNfts?: boolean;
|
||||
}
|
||||
@@ -42,61 +36,82 @@ export const NFTCards: React.FC<NFTCardsProps> = ({
|
||||
isPending,
|
||||
allNfts,
|
||||
}) => {
|
||||
const isMobile = useBreakpointValue({ base: true, md: false });
|
||||
|
||||
const dummyData = useMemo(() => {
|
||||
return Array.from({
|
||||
length: allNfts ? nfts.length : isMobile ? 2 : 3,
|
||||
length: allNfts ? nfts.length : 3,
|
||||
}).map((_, idx) => dummyMetadata(idx));
|
||||
}, [nfts.length, isMobile, allNfts]);
|
||||
}, [nfts.length, allNfts]);
|
||||
|
||||
return (
|
||||
<SimpleGrid
|
||||
gap={{ base: 3, md: 6 }}
|
||||
columns={allNfts ? { base: 2, md: 4 } : { base: 2, md: 3 }}
|
||||
>
|
||||
<div className="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
{(isPending ? dummyData : nfts).map((token) => {
|
||||
const tokenId = token.id.toString();
|
||||
|
||||
return (
|
||||
<GridItem
|
||||
<div
|
||||
key={`${token.chainId}_${token.contractAddress}_${tokenId}`}
|
||||
as={TrackedLink}
|
||||
category={trackingCategory}
|
||||
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
|
||||
_hover={{ opacity: 0.75, textDecoration: "none" }}
|
||||
className="hover:-translate-y-0.5 relative flex h-full cursor-pointer flex-col overflow-hidden rounded-lg bg-background duration-200 hover:scale-[1.01]"
|
||||
>
|
||||
<Card p={0} h="full">
|
||||
<div className="relative aspect-square w-full overflow-hidden rounded-xl">
|
||||
<Skeleton isLoaded={!isPending}>
|
||||
<NFTMediaWithEmptyState
|
||||
metadata={token.metadata}
|
||||
requireInteraction
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
</Skeleton>
|
||||
</div>
|
||||
<Flex p={4} pb={3} gap={1} direction="column">
|
||||
<Skeleton w={!isPending ? "100%" : "50%"} isLoaded={!isPending}>
|
||||
<h2 className="font-semibold tracking-tight">
|
||||
{token.metadata.name}
|
||||
</h2>
|
||||
</Skeleton>
|
||||
<SkeletonText isLoaded={!isPending}>
|
||||
<p className="line-clamp-3 text-muted-foreground text-sm">
|
||||
{token.metadata.description ? (
|
||||
token.metadata.description
|
||||
) : (
|
||||
<i>No description</i>
|
||||
)}
|
||||
</p>
|
||||
</SkeletonText>
|
||||
</Flex>
|
||||
</Card>
|
||||
</GridItem>
|
||||
{/* border */}
|
||||
<div className="absolute inset-0 rounded-lg border border-border" />
|
||||
|
||||
{/* image */}
|
||||
<div className="relative aspect-square w-full overflow-hidden">
|
||||
<SkeletonContainer
|
||||
skeletonData={token.metadata}
|
||||
loadedData={isPending ? undefined : token.metadata}
|
||||
className="h-full w-full rounded-lg"
|
||||
render={(v) => {
|
||||
return (
|
||||
<NFTMediaWithEmptyState
|
||||
metadata={v}
|
||||
requireInteraction
|
||||
width="100%"
|
||||
height="100%"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
{/* title */}
|
||||
<SkeletonContainer
|
||||
skeletonData={token.metadata}
|
||||
loadedData={isPending ? undefined : token.metadata}
|
||||
className="mb-2"
|
||||
render={(v) => {
|
||||
return (
|
||||
<h2 className="font-semibold tracking-tight">
|
||||
<TrackedLinkTW
|
||||
category={trackingCategory}
|
||||
label="view_nft"
|
||||
href={`/${token.chainId}/${token.contractAddress}/nfts/${tokenId}`}
|
||||
className="before:absolute before:inset-0"
|
||||
>
|
||||
{v.name}
|
||||
</TrackedLinkTW>
|
||||
</h2>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Description */}
|
||||
<SkeletonContainer
|
||||
skeletonData={token.metadata}
|
||||
loadedData={isPending ? undefined : token.metadata}
|
||||
render={(v) => {
|
||||
return (
|
||||
<p className="line-clamp-3 text-muted-foreground text-sm">
|
||||
{v.description ? v.description : <i>No description</i>}
|
||||
</p>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</SimpleGrid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-3
@@ -55,7 +55,7 @@ type Options =
|
||||
};
|
||||
|
||||
export async function getClaimPhasesInLegacyFormat(
|
||||
options: BaseTransactionOptions<Options>,
|
||||
options: BaseTransactionOptions<Options> & { isMultiPhase: boolean },
|
||||
): Promise<CombinedClaimCondition[]> {
|
||||
const conditions = await (async () => {
|
||||
switch (options.type) {
|
||||
@@ -64,7 +64,10 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
case "erc721":
|
||||
return ERC721Ext.getClaimConditions(options);
|
||||
case "erc1155":
|
||||
return ERC1155Ext.getClaimConditions(options);
|
||||
return ERC1155Ext.getClaimConditions({
|
||||
...options,
|
||||
singlePhaseDrop: !options.isMultiPhase,
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -126,7 +129,7 @@ export async function getClaimPhasesInLegacyFormat(
|
||||
type PhaseInput = z.input<typeof LegacyClaimConditionInputSchema>;
|
||||
|
||||
export function setClaimPhasesTx(
|
||||
baseOptions: BaseTransactionOptions<Options>,
|
||||
baseOptions: BaseTransactionOptions<Options> & { isSinglePhase?: boolean },
|
||||
rawPhases: PhaseInput[],
|
||||
) {
|
||||
const phases = rawPhases.map((phase) => {
|
||||
@@ -172,6 +175,7 @@ export function setClaimPhasesTx(
|
||||
contract: baseOptions.contract,
|
||||
phases,
|
||||
tokenId: baseOptions.tokenId,
|
||||
singlePhaseDrop: baseOptions.isSinglePhase,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+7
-1
@@ -222,11 +222,15 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
|
||||
const claimConditionsQuery = useReadContract(getClaimPhasesInLegacyFormat, {
|
||||
contract,
|
||||
isMultiPhase,
|
||||
...(isErc20
|
||||
? { type: "erc20", decimals: tokenDecimals.data }
|
||||
: isErc721
|
||||
? { type: "erc721" }
|
||||
: { type: "erc1155", tokenId: BigInt(tokenId || 0) }),
|
||||
: {
|
||||
type: "erc1155",
|
||||
tokenId: BigInt(tokenId || 0),
|
||||
}),
|
||||
});
|
||||
|
||||
const transformedQueryData = useMemo(() => {
|
||||
@@ -370,6 +374,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
const tx = setClaimPhasesTx(
|
||||
{
|
||||
contract,
|
||||
isSinglePhase: !isMultiPhase,
|
||||
...(isErc20
|
||||
? { type: "erc20", decimals: tokenDecimals.data }
|
||||
: isErc721
|
||||
@@ -635,6 +640,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
isErc20={isErc20}
|
||||
contract={contract}
|
||||
tokenId={tokenId}
|
||||
isMultiphase={isMultiPhase}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+3
-2
@@ -10,6 +10,7 @@ interface ClaimConditionsProps {
|
||||
isColumn?: true;
|
||||
isERC20: boolean;
|
||||
twAccount: Account | undefined;
|
||||
isMultiphase: boolean;
|
||||
}
|
||||
export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
|
||||
contract,
|
||||
@@ -17,6 +18,7 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
|
||||
isColumn,
|
||||
isERC20,
|
||||
twAccount,
|
||||
isMultiphase,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
@@ -37,8 +39,7 @@ export const ClaimConditions: React.FC<ClaimConditionsProps> = ({
|
||||
contract={contract}
|
||||
tokenId={tokenId}
|
||||
isColumn={isColumn}
|
||||
// always multi phase!
|
||||
isMultiPhase={true}
|
||||
isMultiPhase={isMultiphase}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ interface ResetClaimEligibilityProps {
|
||||
contract: ThirdwebContract;
|
||||
tokenId?: string;
|
||||
twAccount: Account | undefined;
|
||||
isMultiphase: boolean;
|
||||
}
|
||||
|
||||
export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
|
||||
@@ -25,6 +26,7 @@ export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
|
||||
tokenId,
|
||||
isErc20,
|
||||
twAccount,
|
||||
isMultiphase,
|
||||
}) => {
|
||||
const trackEvent = useTrack();
|
||||
|
||||
@@ -55,6 +57,7 @@ export const ResetClaimEligibility: React.FC<ResetClaimEligibilityProps> = ({
|
||||
return ERC1155Ext.resetClaimEligibility({
|
||||
contract,
|
||||
tokenId: BigInt(tokenId),
|
||||
singlePhaseDrop: !isMultiphase,
|
||||
});
|
||||
}
|
||||
// assume erc 721
|
||||
|
||||
+26
-25
@@ -36,33 +36,34 @@ export function ContractPageLayout(props: {
|
||||
|
||||
return (
|
||||
<ChakraProviderSetup>
|
||||
<SidebarLayout sidebarLinks={sidebarLinks}>
|
||||
<div className="border-border border-b pb-8">
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row">
|
||||
<ContractMetadata
|
||||
contract={contract}
|
||||
chain={chainMetadata}
|
||||
contractMetadata={dashboardContractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
<PrimaryDashboardButton
|
||||
contractAddress={contract.address}
|
||||
chain={contract.chain}
|
||||
contractInfo={{
|
||||
chain: chainMetadata,
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
}}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
/>
|
||||
</div>
|
||||
<DeprecatedAlert chain={chainMetadata} />
|
||||
<div className="border-border border-b py-8">
|
||||
<div className="container flex flex-col gap-4">
|
||||
<div className="flex flex-col justify-between gap-4 md:flex-row">
|
||||
<ContractMetadata
|
||||
contract={contract}
|
||||
chain={chainMetadata}
|
||||
contractMetadata={dashboardContractMetadata}
|
||||
externalLinks={externalLinks}
|
||||
/>
|
||||
<PrimaryDashboardButton
|
||||
contractAddress={contract.address}
|
||||
chain={contract.chain}
|
||||
contractInfo={{
|
||||
chain: chainMetadata,
|
||||
chainSlug: chainMetadata.slug,
|
||||
contractAddress: contract.address,
|
||||
}}
|
||||
teamsAndProjects={teamsAndProjects}
|
||||
client={client}
|
||||
/>
|
||||
</div>
|
||||
<DeprecatedAlert chain={chainMetadata} />
|
||||
</div>
|
||||
<div className="h-8" />
|
||||
<div className="pb-10">{props.children}</div>
|
||||
</div>
|
||||
|
||||
<SidebarLayout sidebarLinks={sidebarLinks}>
|
||||
{props.children}
|
||||
<div className="h-20" />
|
||||
</SidebarLayout>
|
||||
</ChakraProviderSetup>
|
||||
);
|
||||
|
||||
+2
-2
@@ -66,7 +66,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex grow flex-col gap-4">
|
||||
<div className="flex grow flex-col gap-3">
|
||||
{/* Title */}
|
||||
{!data ? (
|
||||
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
@@ -75,7 +75,7 @@ export const MetadataHeader: React.FC<MetadataHeaderProps> = ({
|
||||
) : (
|
||||
<div className="flex flex-col gap-3 lg:flex-row lg:items-center">
|
||||
{data?.name && (
|
||||
<h1 className="line-clamp-2 font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
<h1 className="line-clamp-2 font-bold text-2xl tracking-tight lg:text-3xl">
|
||||
{data.name}
|
||||
</h1>
|
||||
)}
|
||||
|
||||
+1
-1
@@ -89,7 +89,7 @@ function AddToProjectButton(props: {
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button className="gap-2" variant="outline">
|
||||
<Button className="gap-2 bg-card" variant="outline">
|
||||
<PlusIcon className="size-4" />
|
||||
Add To Project
|
||||
</Button>
|
||||
|
||||
+8
-8
@@ -21,7 +21,7 @@ import {
|
||||
} from "./detectedFeatures/permissions";
|
||||
import { supportedERCs } from "./detectedFeatures/supportedERCs";
|
||||
import { type EmbedTypeToShow, getEmbedTypeToShow } from "./getEmbedTypeToShow";
|
||||
import { isAnalyticsSupportedForChain } from "./isAnalyticsSupportedForChain";
|
||||
import { isInsightSupportedForChain } from "./isAnalyticsSupportedForChain";
|
||||
|
||||
export type ContractPageMetadata = {
|
||||
supportedERCs: {
|
||||
@@ -36,7 +36,7 @@ export type ContractPageMetadata = {
|
||||
isPermissionsEnumerableSupported: boolean;
|
||||
isModularCore: boolean;
|
||||
embedType: EmbedTypeToShow;
|
||||
isAnalyticsSupported: boolean;
|
||||
isInsightSupported: boolean;
|
||||
isSplitSupported: boolean;
|
||||
isERC721ClaimConditionsSupported: boolean;
|
||||
isERC20ClaimConditionsSupported: boolean;
|
||||
@@ -47,7 +47,7 @@ export type ContractPageMetadata = {
|
||||
};
|
||||
|
||||
export async function getContractPageMetadata(contract: ThirdwebContract) {
|
||||
return getContractPageMetadataSetup(contract, isAnalyticsSupportedForChain);
|
||||
return getContractPageMetadataSetup(contract, isInsightSupportedForChain);
|
||||
}
|
||||
|
||||
export async function getContractPageMetadataSetup(
|
||||
@@ -56,7 +56,7 @@ export async function getContractPageMetadataSetup(
|
||||
): Promise<ContractPageMetadata> {
|
||||
const [
|
||||
functionSelectorsResult,
|
||||
isAnalyticsSupportedResult,
|
||||
isInsightSupportedResult,
|
||||
contractTypeResult,
|
||||
] = await Promise.allSettled([
|
||||
resolveFunctionSelectors(contract),
|
||||
@@ -69,9 +69,9 @@ export async function getContractPageMetadataSetup(
|
||||
? functionSelectorsResult.value
|
||||
: [];
|
||||
|
||||
const isAnalyticsSupported =
|
||||
isAnalyticsSupportedResult.status === "fulfilled"
|
||||
? isAnalyticsSupportedResult.value
|
||||
const isInsightSupported =
|
||||
isInsightSupportedResult.status === "fulfilled"
|
||||
? isInsightSupportedResult.value
|
||||
: true;
|
||||
|
||||
const contractType =
|
||||
@@ -86,7 +86,7 @@ export async function getContractPageMetadataSetup(
|
||||
isPermissionsEnumerableSupported(functionSelectors),
|
||||
isModularCore: isModularCoreContract(functionSelectors),
|
||||
embedType: getEmbedTypeToShow(functionSelectors),
|
||||
isAnalyticsSupported: isAnalyticsSupported,
|
||||
isInsightSupported: isInsightSupported,
|
||||
isSplitSupported: contractType === "Split",
|
||||
isVoteContract: contractType === "VoteERC20",
|
||||
isERC721ClaimConditionsSupported:
|
||||
|
||||
+1
-1
@@ -47,7 +47,7 @@ export function getContractPageSidebarLinks(data: {
|
||||
{
|
||||
label: "Analytics",
|
||||
href: `${layoutPrefix}/analytics`,
|
||||
hide: !data.metadata.isAnalyticsSupported,
|
||||
hide: !data.metadata.isInsightSupported,
|
||||
exactMatch: true,
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { getVercelEnv } from "lib/vercel-utils";
|
||||
const thirdwebDomain =
|
||||
getVercelEnv() !== "production" ? "thirdweb-dev" : "thirdweb";
|
||||
|
||||
export async function isAnalyticsSupportedForChain(
|
||||
export async function isInsightSupportedForChain(
|
||||
chainId: number,
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
|
||||
+1
@@ -32,6 +32,7 @@ export function AccountPageClient(props: {
|
||||
contract={props.contract}
|
||||
chainMetadata={props.chainMetadata}
|
||||
twAccount={props.twAccount}
|
||||
isInsightSupported={metadataQuery.data.isInsightSupported}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user