Compare commits
52
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 |
@@ -0,0 +1,2 @@
|
||||
public-hoist-pattern[]=*import-in-the-middle*
|
||||
public-hoist-pattern[]=*require-in-the-middle*
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
+46
-44
@@ -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,7 +68,7 @@
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.6.0",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lucide-react": "0.483.0",
|
||||
"lucide-react": "0.487.0",
|
||||
"next": "15.2.4",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-themes": "^0.4.6",
|
||||
@@ -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:*",
|
||||
@@ -109,38 +111,38 @@
|
||||
"@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-react-compiler": "19.0.0-beta-e993439-20250405",
|
||||
"eslint-plugin-storybook": "0.12.0",
|
||||
"knip": "5.46.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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -82,6 +83,7 @@ export function TabButtons(props: {
|
||||
isActive: boolean;
|
||||
isDisabled?: boolean;
|
||||
icon?: React.FC<{ className?: string }>;
|
||||
toolTip?: string;
|
||||
}[];
|
||||
tabClassName?: string;
|
||||
activeTabClassName?: string;
|
||||
@@ -111,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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
+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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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,16 +1,18 @@
|
||||
"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 className="border-b">
|
||||
@@ -33,16 +35,28 @@ export function DeployedContractsPageHeader(props: {
|
||||
<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>
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
import { ArrowUpRightIcon, DownloadIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
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 (
|
||||
@@ -38,18 +40,27 @@ export function DeployViaCLIOrImportCard(props: {
|
||||
className="gap-2 bg-background lg:px-10"
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
<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" />
|
||||
</Link>
|
||||
</TrackedLinkTW>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2 bg-background"
|
||||
onClick={() => setImportModalOpen(true)}
|
||||
onClick={() => {
|
||||
setImportModalOpen(true);
|
||||
trackEvent({
|
||||
category: "contracts-banner",
|
||||
action: "click",
|
||||
label: "import-contract",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DownloadIcon className="size-4" />
|
||||
Import Contract
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { mobileViewport } from "../../../../../stories/utils";
|
||||
import { JoinTeamPageUI } from "./JoinTeamPage";
|
||||
|
||||
const meta = {
|
||||
@@ -16,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 (
|
||||
<div>
|
||||
@@ -36,7 +27,6 @@ function Story() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}}
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+20
-1
@@ -1,4 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import type { Team } from "../../../../@/api/team";
|
||||
import { teamStub } from "../../../../stories/stubs";
|
||||
import { storybookLog } from "../../../../stories/utils";
|
||||
import { TeamOnboardingLayout } from "../onboarding-layout";
|
||||
@@ -36,8 +37,26 @@ export const GrowthPlan: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const AcceleratePlan: Story = {
|
||||
args: {
|
||||
plan: "accelerate",
|
||||
},
|
||||
};
|
||||
|
||||
export const ScalePlan: Story = {
|
||||
args: {
|
||||
plan: "scale",
|
||||
},
|
||||
};
|
||||
|
||||
export const ProPlan: Story = {
|
||||
args: {
|
||||
plan: "pro",
|
||||
},
|
||||
};
|
||||
|
||||
function Story(props: {
|
||||
plan: "free" | "growth" | "starter";
|
||||
plan: Team["billingPlan"];
|
||||
}) {
|
||||
return (
|
||||
<TeamOnboardingLayout currentStep={2}>
|
||||
|
||||
@@ -7,12 +7,12 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
} from "@/components/ui/sheet";
|
||||
import { TabButtons } from "@/components/ui/tabs";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { ArrowRightIcon, CircleArrowUpIcon } from "lucide-react";
|
||||
@@ -74,15 +74,15 @@ export function InviteTeamMembersUI(props: {
|
||||
|
||||
return (
|
||||
<div className="relative flex grow flex-col">
|
||||
<Dialog open={showPlanModal} onOpenChange={setShowPlanModal}>
|
||||
<DialogContent className="lg:max-w-[750px]">
|
||||
<Sheet open={showPlanModal} onOpenChange={setShowPlanModal}>
|
||||
<SheetContent className="!max-w-[1300px] w-full overflow-auto">
|
||||
<InviteModalContent
|
||||
teamSlug={props.team.slug}
|
||||
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
|
||||
trackEvent={props.trackEvent}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
|
||||
<InviteSection
|
||||
inviteTeamMembers={props.inviteTeamMembers}
|
||||
@@ -147,14 +147,17 @@ function InviteModalContent(props: {
|
||||
getBillingCheckoutUrl: GetBillingCheckoutUrlAction;
|
||||
trackEvent: (params: TrackingParams) => void;
|
||||
}) {
|
||||
const [planToShow, setPlanToShow] = useState<"starter" | "growth">("starter");
|
||||
const [planToShow, setPlanToShow] = useState<
|
||||
"starter" | "growth" | "accelerate" | "scale"
|
||||
>("growth");
|
||||
|
||||
const starterPlan = (
|
||||
<PricingCard
|
||||
billingPlan="starter"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Get Started for free",
|
||||
title: "Get Started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
@@ -163,10 +166,6 @@ function InviteModalContent(props: {
|
||||
plan: "starter",
|
||||
});
|
||||
},
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: "starterPlan",
|
||||
},
|
||||
}}
|
||||
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
|
||||
/>
|
||||
@@ -177,11 +176,8 @@ function InviteModalContent(props: {
|
||||
billingPlan="growth"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Get Started with Growth",
|
||||
tracking: {
|
||||
category: "account",
|
||||
label: "growthPlan",
|
||||
},
|
||||
title: "Get Started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
@@ -190,33 +186,75 @@ function InviteModalContent(props: {
|
||||
plan: "growth",
|
||||
});
|
||||
},
|
||||
variant: "default",
|
||||
}}
|
||||
highlighted
|
||||
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
const acceleratePlan = (
|
||||
<PricingCard
|
||||
billingPlan="accelerate"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Get started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "upgradePlan",
|
||||
label: "attempt",
|
||||
plan: "accelerate",
|
||||
});
|
||||
},
|
||||
}}
|
||||
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
const scalePlan = (
|
||||
<PricingCard
|
||||
billingPlan="scale"
|
||||
teamSlug={props.teamSlug}
|
||||
cta={{
|
||||
title: "Get started",
|
||||
type: "checkout",
|
||||
onClick() {
|
||||
props.trackEvent({
|
||||
category: "teamOnboarding",
|
||||
action: "upgradePlan",
|
||||
label: "attempt",
|
||||
plan: "scale",
|
||||
});
|
||||
},
|
||||
}}
|
||||
getBillingCheckoutUrl={props.getBillingCheckoutUrl}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl">Choose a plan</DialogTitle>
|
||||
|
||||
<DialogDescription className="leading-relaxed">
|
||||
<SheetHeader className="space-y-0.5">
|
||||
<SheetTitle className="text-left text-2xl tracking-tight">
|
||||
Choose a plan
|
||||
</SheetTitle>
|
||||
<SheetDescription className="text-left leading-relaxed">
|
||||
Get started with the free Starter plan or upgrade to Growth plan for
|
||||
increased limits and advanced features.{" "}
|
||||
<UnderlineLink href="https://thirdweb.com/pricing" target="_blank">
|
||||
Learn more about pricing
|
||||
</UnderlineLink>
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
|
||||
<div className="h-6" />
|
||||
<div className="h-5" />
|
||||
|
||||
{/* Desktop */}
|
||||
<div className="hidden grid-cols-1 gap-6 md:grid-cols-2 md:gap-4 lg:grid">
|
||||
<div className="hidden grid-cols-1 gap-6 md:grid-cols-4 md:gap-4 lg:grid">
|
||||
{starterPlan}
|
||||
{growthPlan}
|
||||
{acceleratePlan}
|
||||
{scalePlan}
|
||||
</div>
|
||||
|
||||
{/* Mobile */}
|
||||
@@ -233,11 +271,23 @@ function InviteModalContent(props: {
|
||||
onClick: () => setPlanToShow("growth"),
|
||||
isActive: planToShow === "growth",
|
||||
},
|
||||
{
|
||||
name: "Accelerate",
|
||||
onClick: () => setPlanToShow("accelerate"),
|
||||
isActive: planToShow === "accelerate",
|
||||
},
|
||||
{
|
||||
name: "Scale",
|
||||
onClick: () => setPlanToShow("scale"),
|
||||
isActive: planToShow === "scale",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<div className="h-4" />
|
||||
{planToShow === "starter" && starterPlan}
|
||||
{planToShow === "growth" && growthPlan}
|
||||
{planToShow === "accelerate" && acceleratePlan}
|
||||
{planToShow === "scale" && scalePlan}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { teamStub } from "../../../../stories/stubs";
|
||||
import { storybookLog } from "../../../../stories/utils";
|
||||
@@ -59,7 +58,6 @@ function Story(props: {
|
||||
return teamStub("foo", "free");
|
||||
}}
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</TeamOnboardingLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -155,6 +155,8 @@ export function TeamInfoFormUI(props: {
|
||||
});
|
||||
}
|
||||
|
||||
const maxTeamNameLength = 32;
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border bg-card ">
|
||||
<Form {...form}>
|
||||
@@ -199,6 +201,7 @@ export function TeamInfoFormUI(props: {
|
||||
placeholder="Company Inc."
|
||||
autoComplete="off"
|
||||
{...field}
|
||||
maxLength={maxTeamNameLength}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
|
||||
@@ -2,35 +2,51 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { subDays } from "date-fns";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub, randomLorem } from "../../../../../stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../../stories/utils";
|
||||
import { ChatPageLayout } from "../../components/ChatPageLayout";
|
||||
import { ChatHistoryPageUI } from "./ChatHistoryPage";
|
||||
|
||||
const meta = {
|
||||
title: "Nebula/history",
|
||||
component: Story,
|
||||
component: Variant,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThirdwebProvider>
|
||||
<Story />
|
||||
</ThirdwebProvider>
|
||||
),
|
||||
],
|
||||
} satisfies Meta<typeof Variant>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const TenChats: Story = {
|
||||
args: {
|
||||
length: 10,
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
export const ZeroChats: Story = {
|
||||
args: {
|
||||
length: 0,
|
||||
},
|
||||
};
|
||||
|
||||
export const OneChat: Story = {
|
||||
args: {
|
||||
length: 1,
|
||||
},
|
||||
};
|
||||
|
||||
export const PrefillSearch: Story = {
|
||||
args: {
|
||||
length: 10,
|
||||
},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
prefillSearch: "xxxxxxxxxxxx",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -51,30 +67,6 @@ function createRandomSessions(length: number) {
|
||||
return sessions;
|
||||
}
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="flex flex-col gap-10 py-10">
|
||||
<BadgeContainer label="10 chats">
|
||||
<Variant length={10} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="0 chats">
|
||||
<Variant length={0} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="1 chat">
|
||||
<Variant length={1} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No search result">
|
||||
<Variant length={10} prefillSearch="xxxxxxxxxxxx" />
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
length: number;
|
||||
prefillSearch?: string;
|
||||
@@ -85,7 +77,6 @@ function Variant(props: {
|
||||
accountAddress="0x1234567890"
|
||||
authToken="xxxxxxxx"
|
||||
sessions={createRandomSessions(props.length)}
|
||||
className="h-[700px] border-b lg:h-[800px]"
|
||||
>
|
||||
<ChatHistoryPageUI
|
||||
sessions={createRandomSessions(props.length)}
|
||||
|
||||
@@ -1,32 +1,51 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster } from "sonner";
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub, randomLorem } from "../../../../stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import type { TruncatedSessionInfo } from "../api/types";
|
||||
import { ChatPageLayout } from "./ChatPageLayout";
|
||||
|
||||
const meta = {
|
||||
title: "Nebula/ChatPageLayout",
|
||||
component: Story,
|
||||
component: Variant,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThirdwebProvider>
|
||||
<Story />
|
||||
</ThirdwebProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
} satisfies Meta<typeof Variant>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
export const NoRecentChats: Story = {
|
||||
args: {
|
||||
sessions: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
export const ThreeChats: Story = {
|
||||
args: {
|
||||
sessions: generateRandomSessions(3),
|
||||
},
|
||||
};
|
||||
|
||||
export const ThirtyChats: Story = {
|
||||
args: {
|
||||
sessions: generateRandomSessions(30),
|
||||
},
|
||||
};
|
||||
|
||||
export const NoEmail: Story = {
|
||||
args: {
|
||||
sessions: generateRandomSessions(30),
|
||||
noEmail: true,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -39,48 +58,26 @@ function generateRandomSessions(count: number): TruncatedSessionInfo[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<div className="flex flex-col gap-10 py-10 lg:container">
|
||||
<Variant label="No Recent Chats" sessions={[]} />
|
||||
<Variant label="3 Chats" sessions={generateRandomSessions(3)} />
|
||||
<Variant label="30 Chats" sessions={generateRandomSessions(30)} />
|
||||
<Variant
|
||||
label="No Email"
|
||||
sessions={generateRandomSessions(30)}
|
||||
noEmail
|
||||
/>
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
label: string;
|
||||
sessions: TruncatedSessionInfo[];
|
||||
noEmail?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
<ChatPageLayout
|
||||
accountAddress="0xC569B9FD77d132e10954cA5E6EF617414e314b11"
|
||||
authToken="xxxxx"
|
||||
sessions={props.sessions}
|
||||
className="h-[200px] lg:h-[800px] lg:border"
|
||||
account={accountStub(
|
||||
props.noEmail
|
||||
? {
|
||||
email: undefined,
|
||||
}
|
||||
: undefined,
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
CHILDREN
|
||||
</div>
|
||||
</ChatPageLayout>
|
||||
<Toaster richColors />
|
||||
</BadgeContainer>
|
||||
<ChatPageLayout
|
||||
accountAddress="0xC569B9FD77d132e10954cA5E6EF617414e314b11"
|
||||
authToken="xxxxx"
|
||||
sessions={props.sessions}
|
||||
account={accountStub(
|
||||
props.noEmail
|
||||
? {
|
||||
email: undefined,
|
||||
}
|
||||
: undefined,
|
||||
)}
|
||||
>
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
CHILDREN
|
||||
</div>
|
||||
</ChatPageLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { ChatBar } from "./ChatBar";
|
||||
|
||||
const meta = {
|
||||
@@ -15,20 +15,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-[800px] flex-col gap-14 py-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-14 py-10">
|
||||
<BadgeContainer label="Not Streaming">
|
||||
<ChatBar
|
||||
abortChatStream={() => {}}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { Toaster } from "sonner";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub, randomLorem } from "../../../../stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { type ChatMessage, Chats } from "./Chats";
|
||||
|
||||
const meta = {
|
||||
@@ -19,17 +18,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 markdownExample = `\
|
||||
# Heading 1
|
||||
## Heading 2
|
||||
@@ -245,8 +237,6 @@ function Story() {
|
||||
]}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import type { NebulaContext } from "../api/chat";
|
||||
import ContextFiltersButton from "./ContextFilters";
|
||||
|
||||
@@ -18,20 +17,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 py-10 lg:p-10">
|
||||
<div className="container flex max-w-6xl flex-col gap-8 py-10">
|
||||
<Variant contextFilters={undefined} label="No Filters Set" />
|
||||
|
||||
<Variant
|
||||
@@ -65,7 +57,6 @@ function Story() {
|
||||
}}
|
||||
label="chains + wallet"
|
||||
/>
|
||||
<Toaster richColors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-9
@@ -1,5 +1,4 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { mobileViewport } from "../../../../stories/utils";
|
||||
import { EmptyStateChatPageContent } from "./EmptyStateChatPageContent";
|
||||
|
||||
const meta = {
|
||||
@@ -15,17 +14,10 @@ const meta = {
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
export const Default: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[800px] grow flex-col justify-center overflow-hidden">
|
||||
|
||||
+2
-9
@@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { ConnectButton, ThirdwebProvider } from "thirdweb/react";
|
||||
import { accountStub } from "../../../../stories/stubs";
|
||||
import { BadgeContainer, mobileViewport } from "../../../../stories/utils";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import {
|
||||
ExecuteTransactionCardLayout,
|
||||
type TxStatus,
|
||||
@@ -22,17 +22,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 client = getThirdwebClient();
|
||||
|
||||
const exampleTxHash =
|
||||
|
||||
-1
@@ -7,7 +7,6 @@ import {
|
||||
const meta = {
|
||||
title: "Banners/Billing Alerts",
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
|
||||
+15
-2
@@ -1,4 +1,6 @@
|
||||
import {} from "@/components/ui/breadcrumb";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getTeamBySlug } from "../../../../../../../../../../@/api/team";
|
||||
import { getAuthToken } from "../../../../../../../../../api/lib/getAuthToken";
|
||||
import { loginRedirect } from "../../../../../../../../../login/loginRedirect";
|
||||
import { AddPartnerForm } from "../components/client/add-partner-form.client";
|
||||
@@ -10,12 +12,19 @@ export default async function AddPartnerPage({
|
||||
params: Promise<{ slug: string; team_slug: string }>;
|
||||
}) {
|
||||
const { slug, team_slug } = await params;
|
||||
const authToken = await getAuthToken();
|
||||
const [authToken, team] = await Promise.all([
|
||||
getAuthToken(),
|
||||
getTeamBySlug(team_slug),
|
||||
]);
|
||||
|
||||
if (!authToken) {
|
||||
loginRedirect(`/team/${team_slug}/~/ecosystem/${slug}`);
|
||||
}
|
||||
|
||||
if (!team) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
const teamSlug = team_slug;
|
||||
const ecosystemSlug = slug;
|
||||
|
||||
@@ -32,7 +41,11 @@ export default async function AddPartnerPage({
|
||||
<h1 className="mb-6 font-semibold text-2xl tracking-tight">
|
||||
Add New Partner
|
||||
</h1>
|
||||
<AddPartnerForm ecosystem={ecosystem} authToken={authToken} />
|
||||
<AddPartnerForm
|
||||
ecosystem={ecosystem}
|
||||
authToken={authToken}
|
||||
teamId={team.id}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
+13
-2
@@ -7,7 +7,12 @@ import { IntegrationPermissionsSection } from "../server/integration-permissions
|
||||
export function EcosystemPermissionsPage({
|
||||
params,
|
||||
authToken,
|
||||
}: { params: { slug: string; team_slug: string }; authToken: string }) {
|
||||
teamId,
|
||||
}: {
|
||||
params: { slug: string; team_slug: string };
|
||||
authToken: string;
|
||||
teamId: string;
|
||||
}) {
|
||||
const { data: ecosystem } = useEcosystem({
|
||||
slug: params.slug,
|
||||
teamIdOrSlug: params.team_slug,
|
||||
@@ -18,13 +23,19 @@ export function EcosystemPermissionsPage({
|
||||
<IntegrationPermissionsSection
|
||||
ecosystem={ecosystem}
|
||||
authToken={authToken}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<AuthOptionsSection
|
||||
ecosystem={ecosystem}
|
||||
authToken={authToken}
|
||||
teamId={teamId}
|
||||
/>
|
||||
<AuthOptionsSection ecosystem={ecosystem} authToken={authToken} />
|
||||
{ecosystem?.permission === "PARTNER_WHITELIST" && (
|
||||
<EcosystemPartnersSection
|
||||
teamSlug={params.team_slug}
|
||||
ecosystem={ecosystem}
|
||||
authToken={authToken}
|
||||
teamId={teamId}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user