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