Compare commits
99
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d66568467d | ||
|
|
6bd968362a | ||
|
|
17488b8139 | ||
|
|
776f5aaeda | ||
|
|
ecaa304a81 | ||
|
|
377f46c303 | ||
|
|
b52049c20a | ||
|
|
a67e980ac3 | ||
|
|
4c8333d4a3 | ||
|
|
e95ddcdd3e | ||
|
|
7973d6450c | ||
|
|
017b13f444 | ||
|
|
901c3a102b | ||
|
|
3253a532f8 | ||
|
|
0e5d120909 | ||
|
|
f368793375 | ||
|
|
b5227c9a8f | ||
|
|
0998fc737b | ||
|
|
3df0541582 | ||
|
|
2591c75b56 | ||
|
|
55edd4cb8e | ||
|
|
e0eea33d06 | ||
|
|
d7e36a8afd | ||
|
|
bcbfb2065f | ||
|
|
261102b798 | ||
|
|
42910372fc | ||
|
|
1554e377eb | ||
|
|
510be22cb6 | ||
|
|
d4430d4b2f | ||
|
|
ed58c4c930 | ||
|
|
8e22cf8812 | ||
|
|
565dbb9ef3 | ||
|
|
f01de732fb | ||
|
|
a00233d1ba | ||
|
|
acc7303c05 | ||
|
|
304f2e6b8e | ||
|
|
5585defbe4 | ||
|
|
10ca0bcd47 | ||
|
|
7b21f1b632 | ||
|
|
f7a4a466d5 | ||
|
|
895b4d145f | ||
|
|
e958e75df5 | ||
|
|
b620deecfe | ||
|
|
5218313b19 | ||
|
|
731089b29d | ||
|
|
92e55c8b3d | ||
|
|
5de5c44985 | ||
|
|
3488281958 | ||
|
|
c93755e6f6 | ||
|
|
55a00c441e | ||
|
|
761417cc1a | ||
|
|
f331f01225 | ||
|
|
7bbe60f2be | ||
|
|
9f9b67da5f | ||
|
|
24a53e3b82 | ||
|
|
9ea155e4f4 | ||
|
|
fae444da66 | ||
|
|
b30c4d0484 | ||
|
|
6b853df3d2 | ||
|
|
e841bfe081 | ||
|
|
e0c641c9cd | ||
|
|
a1fc436a92 | ||
|
|
b30177c518 | ||
|
|
040e478ad6 | ||
|
|
6771cfed62 | ||
|
|
ff188fd49d | ||
|
|
512eeb5cf0 | ||
|
|
a1707f66f3 | ||
|
|
f31801e790 | ||
|
|
ef56304195 | ||
|
|
daae1dce85 | ||
|
|
afb6343198 | ||
|
|
7d34a8f50c | ||
|
|
9584885f42 | ||
|
|
c6310e60d5 | ||
|
|
dc0ea8a286 | ||
|
|
e4b40080f4 | ||
|
|
a1b518c676 | ||
|
|
8c1fa1b7b4 | ||
|
|
453e45bd52 | ||
|
|
07af647067 | ||
|
|
535e1df9a3 | ||
|
|
a9375ce593 | ||
|
|
60eec0344d | ||
|
|
2e641f816a | ||
|
|
c8ad066cf2 | ||
|
|
0bba41446b | ||
|
|
6dda1c242e | ||
|
|
6b1f4184a4 | ||
|
|
9910a0230a | ||
|
|
9572db007a | ||
|
|
5de541878c | ||
|
|
46d0b4bdc0 | ||
|
|
1e7e32f2cb | ||
|
|
d34357e0a9 | ||
|
|
03ce8816ce | ||
|
|
2d99708c45 | ||
|
|
aa46e27d7c | ||
|
|
9f9410a77a |
@@ -8,6 +8,12 @@ playground:
|
||||
packages:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "packages/**/*"
|
||||
sdk:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "packages/thirdweb/**/*"
|
||||
portal:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "apps/portal/**/*"
|
||||
"Ecosystem Portal":
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "apps/wallet-ui/**/*"
|
||||
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
- run: pnpm test
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
directory: packages/
|
||||
flags: packages
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
echo "Pull Request Body: $pr_body"
|
||||
|
||||
if echo "$pr_body" | grep -iE "CNCT|DASH"; then
|
||||
if echo "$pr_body" | grep -iE "CNCT|DASH|BLOCK"; then
|
||||
echo "Linked issue found in the pull request body."
|
||||
else
|
||||
echo "No linked issue found in the pull request body."
|
||||
|
||||
@@ -11,5 +11,5 @@ jobs:
|
||||
with:
|
||||
stale-issue-message: 'This issue has been inactive for 30 days. It is now marked as stale and will be closed in 5 days if no further activity occurs.'
|
||||
stale-pr-message: 'This PR has been inactive for 30 days. It is now marked as stale and will be closed in 5 days if no further activity occurs.'
|
||||
days-before-stale: 30
|
||||
days-before-close: 5
|
||||
days-before-stale: 7
|
||||
days-before-close: 2
|
||||
|
||||
Vendored
+3
@@ -24,5 +24,8 @@
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,4 +92,6 @@ API_SERVER_SECRET=""
|
||||
# Used for the Faucet page (/<chain_id>)
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY=""
|
||||
TURNSTILE_SECRET_KEY=""
|
||||
REDIS_URL=""
|
||||
REDIS_URL=""
|
||||
|
||||
ANALYTICS_SERVICE_URL=""
|
||||
@@ -2,15 +2,13 @@ import type { Preview } from "@storybook/react";
|
||||
import "@/styles/globals.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
import { Inter as interFont } from "next/font/google";
|
||||
// biome-ignore lint/style/useImportType: <explanation>
|
||||
import React, { useState } from "react";
|
||||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { Button } from "../src/@/components/ui/button";
|
||||
|
||||
// Note: Wrapping the Stoy with AppRouterProviders makes the storybook server time SUPER SLOW
|
||||
// so let's just not have it there - all stories should be independent from context anyway and just rely on props
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const fontSans = interFont({
|
||||
@@ -31,9 +29,16 @@ const preview: Preview = {
|
||||
decorators: [
|
||||
(Story) => {
|
||||
return (
|
||||
<StoryLayout>
|
||||
<Story />
|
||||
</StoryLayout>
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
disableTransitionOnChange
|
||||
enableSystem={false}
|
||||
defaultTheme="dark"
|
||||
>
|
||||
<StoryLayout>
|
||||
<Story />
|
||||
</StoryLayout>
|
||||
</ThemeProvider>
|
||||
);
|
||||
},
|
||||
],
|
||||
@@ -44,34 +49,36 @@ export default preview;
|
||||
function StoryLayout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [theme, setTheme] = useState<"light" | "dark">("dark");
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
document.body.className = `font-sans antialiased ${fontSans.variable} ${theme === "dark" ? " dark" : ""}`;
|
||||
}, [theme]);
|
||||
document.body.className = `font-sans antialiased ${fontSans.variable}`;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<div className="min-h-screen flex flex-col bg-background text-foreground min-w-0">
|
||||
<div className="flex justify-end p-4 border-b gap-2">
|
||||
<div className="flex min-h-screen min-w-0 flex-col bg-background text-foreground">
|
||||
<div className="flex justify-end gap-2 border-b p-4">
|
||||
<Button
|
||||
onClick={() => setTheme("dark")}
|
||||
size="sm"
|
||||
variant={theme === "dark" ? "default" : "ghost"}
|
||||
variant={theme === "dark" ? "default" : "outline"}
|
||||
className="h-auto w-auto rounded-full p-2"
|
||||
>
|
||||
<MoonIcon className="size-5" />
|
||||
<MoonIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={() => setTheme("light")}
|
||||
size="sm"
|
||||
variant={theme === "light" ? "default" : "ghost"}
|
||||
variant={theme === "light" ? "default" : "outline"}
|
||||
className="h-auto w-auto rounded-full p-2"
|
||||
>
|
||||
<SunIcon className="size-5" />
|
||||
<SunIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="grow flex flex-col min-w-0">{props.children}</div>
|
||||
<div className="flex min-w-0 grow flex-col">{props.children}</div>
|
||||
</div>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
module.exports = [
|
||||
// -- home
|
||||
"/",
|
||||
"/pricing",
|
||||
// -- product landing pages --
|
||||
// -- connect
|
||||
"/connect/sign-in",
|
||||
"/nebula",
|
||||
// -- contracts
|
||||
"/contracts/modular-contracts",
|
||||
"/contracts/explore",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||
import type { SentryBuildOptions } from "@sentry/nextjs";
|
||||
import type { NextConfig } from "next";
|
||||
import type { RemotePattern } from "next/dist/shared/lib/image-config";
|
||||
import FRAMER_PATHS from "./framer-rewrites";
|
||||
import getRedirects from "./redirects";
|
||||
|
||||
const ContentSecurityPolicy = `
|
||||
default-src 'self';
|
||||
@@ -37,15 +41,9 @@ const securityHeaders = [
|
||||
},
|
||||
];
|
||||
|
||||
const redirects = require("./redirects");
|
||||
const FRAMER_PATHS = require("./framer-rewrites");
|
||||
|
||||
/**
|
||||
* @returns {import('next').RemotePattern[]}
|
||||
*/
|
||||
function determineIpfsGateways() {
|
||||
// add the clientId ipfs gateways
|
||||
const remotePatterns = [];
|
||||
const remotePatterns: RemotePattern[] = [];
|
||||
if (process.env.API_ROUTES_CLIENT_ID) {
|
||||
remotePatterns.push({
|
||||
protocol: "https",
|
||||
@@ -84,23 +82,40 @@ function determineIpfsGateways() {
|
||||
return remotePatterns;
|
||||
}
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const moduleExports = {
|
||||
webpack: (config, { dev }) => {
|
||||
if (config.cache && !dev) {
|
||||
config.cache = Object.freeze({
|
||||
type: "memory",
|
||||
});
|
||||
config.cache.maxMemoryGenerations = 0;
|
||||
}
|
||||
config.externals.push("pino-pretty");
|
||||
config.module = {
|
||||
...config.module,
|
||||
exprContextCritical: false,
|
||||
};
|
||||
// Important: return the modified config
|
||||
return config;
|
||||
},
|
||||
const SENTRY_OPTIONS: SentryBuildOptions = {
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
|
||||
org: "thirdweb-dev",
|
||||
project: "dashboard",
|
||||
// An auth token is required for uploading source maps.
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
// Suppresses source map uploading logs during build
|
||||
silent: true,
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
|
||||
tunnelRoute: "/err",
|
||||
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: true,
|
||||
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
disableLogger: true,
|
||||
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors.
|
||||
// See the following for more information:
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: false,
|
||||
};
|
||||
|
||||
const baseNextConfig: NextConfig = {
|
||||
serverExternalPackages: ["pino-pretty"],
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@@ -117,7 +132,7 @@ const moduleExports = {
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
return redirects();
|
||||
return getRedirects();
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
@@ -151,74 +166,57 @@ const moduleExports = {
|
||||
...determineIpfsGateways(),
|
||||
],
|
||||
},
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
scrollRestoration: true,
|
||||
webpackBuildWorker: true,
|
||||
serverSourceMaps: false,
|
||||
},
|
||||
cacheMaxMemorySize: 0,
|
||||
compiler: {
|
||||
emotion: true,
|
||||
},
|
||||
productionBrowserSourceMaps: false,
|
||||
reactStrictMode: true,
|
||||
};
|
||||
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
const { withPlausibleProxy } = require("next-plausible");
|
||||
function getConfig(): NextConfig {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { withPlausibleProxy } = require("next-plausible");
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { withSentryConfig } = require("@sentry/nextjs");
|
||||
return withBundleAnalyzer(
|
||||
withPlausibleProxy({
|
||||
customDomain: "https://pl.thirdweb.com",
|
||||
scriptName: "pl",
|
||||
})(
|
||||
withSentryConfig(
|
||||
{
|
||||
...baseNextConfig,
|
||||
experimental: {
|
||||
webpackBuildWorker: true,
|
||||
},
|
||||
// @ts-expect-error - this is a valid option
|
||||
webpack: (config, { dev }) => {
|
||||
if (config.cache && !dev) {
|
||||
config.cache = Object.freeze({
|
||||
type: "filesystem",
|
||||
maxMemoryGenerations: 0,
|
||||
});
|
||||
}
|
||||
config.externals.push("pino-pretty");
|
||||
config.module = {
|
||||
...config.module,
|
||||
exprContextCritical: false,
|
||||
};
|
||||
// Important: return the modified config
|
||||
return config;
|
||||
},
|
||||
},
|
||||
SENTRY_OPTIONS,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
// otherwise return the base
|
||||
return baseNextConfig;
|
||||
}
|
||||
|
||||
// we only want sentry on production environments
|
||||
const wSentry =
|
||||
process.env.NODE_ENV === "production" ? withSentryConfig : (x) => x;
|
||||
|
||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
});
|
||||
|
||||
module.exports = withBundleAnalyzer(
|
||||
withPlausibleProxy({
|
||||
customDomain: "https://pl.thirdweb.com",
|
||||
scriptName: "pl",
|
||||
})(
|
||||
wSentry(moduleExports, {
|
||||
// For all available options, see:
|
||||
// https://github.com/getsentry/sentry-webpack-plugin#options
|
||||
|
||||
org: "thirdweb-dev",
|
||||
project: "dashboard",
|
||||
// An auth token is required for uploading source maps.
|
||||
authToken: process.env.SENTRY_AUTH_TOKEN,
|
||||
// Suppresses source map uploading logs during build
|
||||
silent: true,
|
||||
// For all available options, see:
|
||||
// https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/
|
||||
|
||||
// Upload a larger set of source maps for prettier stack traces (increases build time)
|
||||
widenClientFileUpload: true,
|
||||
|
||||
// Transpiles SDK to be compatible with IE11 (increases bundle size)
|
||||
transpileClientSDK: false,
|
||||
|
||||
// Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load)
|
||||
tunnelRoute: "/err",
|
||||
|
||||
// Hides source maps from generated client bundles
|
||||
hideSourceMaps: true,
|
||||
|
||||
// Automatically tree-shake Sentry logger statements to reduce bundle size
|
||||
disableLogger: true,
|
||||
|
||||
// Enables automatic instrumentation of Vercel Cron Monitors.
|
||||
// See the following for more information:
|
||||
// https://docs.sentry.io/product/crons/
|
||||
// https://vercel.com/docs/cron-jobs
|
||||
automaticVercelMonitors: false,
|
||||
|
||||
/**
|
||||
* Disables the Sentry Webpack plugin on the server.
|
||||
* See: https://github.com/getsentry/sentry-javascript/issues/10468#issuecomment-2004710692
|
||||
*/
|
||||
disableServerWebpackPlugin: true,
|
||||
}),
|
||||
),
|
||||
);
|
||||
export default getConfig();
|
||||
+26
-29
@@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"preinstall": "npx only-allow pnpm",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"build": "NODE_OPTIONS=--max-old-space-size=6144 next build",
|
||||
"start": "next start",
|
||||
"format": "biome format ./src --write",
|
||||
"lint": "biome check ./src && knip && eslint ./src",
|
||||
@@ -45,12 +45,10 @@
|
||||
"@radix-ui/react-separator": "^1.1.0",
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.3",
|
||||
"@sentry/nextjs": "8.37.1",
|
||||
"@radix-ui/react-tooltip": "1.1.4",
|
||||
"@sentry/nextjs": "8.38.0",
|
||||
"@shazow/whatsabi": "^0.16.0",
|
||||
"@stripe/react-stripe-js": "^2.8.1",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
"@tanstack/react-query": "5.59.19",
|
||||
"@tanstack/react-query": "5.60.2",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "^1.4.2",
|
||||
@@ -63,23 +61,21 @@
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "4.1.0",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "11.11.11",
|
||||
"framer-motion": "11.11.17",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.4.1",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"lucide-react": "0.454.0",
|
||||
"next": "15.0.2",
|
||||
"next-plausible": "^3.12.3",
|
||||
"lucide-react": "0.456.0",
|
||||
"next": "15.0.3",
|
||||
"next-plausible": "^3.12.4",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-themes": "^0.4.3",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"papaparse": "^5.4.1",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.67.1",
|
||||
"prism-react-renderer": "^2.3.1",
|
||||
"prismjs": "^1.29.0",
|
||||
"qrcode": "^1.5.3",
|
||||
"react": "19.0.0-rc-69d4b800-20241021",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
@@ -87,13 +83,14 @@
|
||||
"react-dom": "19.0.0-rc-69d4b800-20241021",
|
||||
"react-dropzone": "^14.3.5",
|
||||
"react-error-boundary": "^4.1.2",
|
||||
"react-hook-form": "7.52.0",
|
||||
"react-hook-form": "7.53.2",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "^2.13.3",
|
||||
"remark-gfm": "^4.0.0",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.22.2",
|
||||
"sonner": "^1.7.0",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
@@ -107,18 +104,18 @@
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "3.2.2",
|
||||
"@next/bundle-analyzer": "15.0.2",
|
||||
"@next/eslint-plugin-next": "15.0.2",
|
||||
"@next/bundle-analyzer": "15.0.3",
|
||||
"@next/eslint-plugin-next": "15.0.3",
|
||||
"@playwright/test": "1.48.2",
|
||||
"@storybook/addon-essentials": "8.4.2",
|
||||
"@storybook/addon-interactions": "8.4.2",
|
||||
"@storybook/addon-links": "8.4.2",
|
||||
"@storybook/addon-onboarding": "8.4.2",
|
||||
"@storybook/addon-viewport": "8.4.2",
|
||||
"@storybook/blocks": "8.4.2",
|
||||
"@storybook/nextjs": "8.4.2",
|
||||
"@storybook/react": "8.4.2",
|
||||
"@storybook/test": "8.4.2",
|
||||
"@storybook/addon-essentials": "8.4.4",
|
||||
"@storybook/addon-interactions": "8.4.4",
|
||||
"@storybook/addon-links": "8.4.4",
|
||||
"@storybook/addon-onboarding": "8.4.4",
|
||||
"@storybook/addon-viewport": "8.4.4",
|
||||
"@storybook/blocks": "8.4.4",
|
||||
"@storybook/nextjs": "8.4.4",
|
||||
"@storybook/react": "8.4.4",
|
||||
"@storybook/test": "8.4.4",
|
||||
"@types/color": "4.2.0",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
@@ -135,13 +132,13 @@
|
||||
"checkly": "^4.8.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.9.3",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-8a03594-20241020",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
|
||||
"eslint-plugin-storybook": "^0.9.0",
|
||||
"knip": "5.36.2",
|
||||
"knip": "5.37.0",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.47",
|
||||
"storybook": "8.4.2",
|
||||
"tailwindcss": "3.4.14",
|
||||
"postcss": "8.4.49",
|
||||
"storybook": "8.4.4",
|
||||
"tailwindcss": "3.4.15",
|
||||
"typescript": "5.6.3"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
+69
-124
@@ -1,5 +1,69 @@
|
||||
const legacyDashboardToTeamRedirects = [
|
||||
{
|
||||
source: "/dashboard",
|
||||
destination: "/team",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/contracts/:path*",
|
||||
destination: "/team/~/~/contracts",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/connect/ecosystem/:path*",
|
||||
destination: "/team/~/~/ecosystem/:path*",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/engine/:path*",
|
||||
destination: "/team/~/~/engine/:path*",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings/api-keys",
|
||||
destination: "/team/~/~/projects",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings/devices",
|
||||
destination: "/account/devices",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings/billing",
|
||||
destination: "/team/~/~/settings/billing",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings/gas-credits",
|
||||
destination: "/team/~/~/settings/credits",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings/usage",
|
||||
destination: "/team/~/~/usage",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings/storage",
|
||||
destination: "/team/~/~/usage/storage",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings/notifications",
|
||||
destination: "/team/~/~/settings/notifications",
|
||||
permanent: false,
|
||||
},
|
||||
// rest of the /dashboard/* routes
|
||||
{
|
||||
source: "/dashboard/:path*",
|
||||
destination: "/team",
|
||||
permanent: false,
|
||||
},
|
||||
];
|
||||
|
||||
/** @type {import('next').NextConfig['redirects']} */
|
||||
function redirects() {
|
||||
async function redirects() {
|
||||
return [
|
||||
{
|
||||
source: "/portal/:match*",
|
||||
@@ -11,16 +75,6 @@ function redirects() {
|
||||
destination: "/solutions/chains",
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/publish/:path*",
|
||||
destination: "/contracts/publish/:path*",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/mumbai/publish/:path*",
|
||||
destination: "/contracts/publish/:path*",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/contracts/release",
|
||||
destination: "/contracts/publish",
|
||||
@@ -97,23 +151,8 @@ function redirects() {
|
||||
destination: "/polygon-zkevm",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/settings",
|
||||
destination: "/dashboard/settings/api-keys",
|
||||
permanent: false,
|
||||
},
|
||||
// backwards compat: page moved to pages/settings/api-key
|
||||
{
|
||||
source: "/dashboard/api-keys",
|
||||
destination: "/dashboard/settings/api-keys",
|
||||
permanent: false,
|
||||
},
|
||||
// backwards compat: page moved to pages/settings/devices
|
||||
{
|
||||
source: "/dashboard/settings/account",
|
||||
destination: "/dashboard/settings/devices",
|
||||
permanent: false,
|
||||
},
|
||||
|
||||
{
|
||||
source: "/template/nft-drop",
|
||||
destination: "/template/erc721",
|
||||
@@ -121,17 +160,12 @@ function redirects() {
|
||||
},
|
||||
{
|
||||
source: "/create-api-key",
|
||||
destination: "/dashboard/settings/api-keys",
|
||||
destination: "/team/~/~/projects",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/settings",
|
||||
destination: "/dashboard/settings/api-keys",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/connect",
|
||||
destination: "/dashboard/connect/analytics",
|
||||
destination: "/team/~/~/projects",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
@@ -139,61 +173,6 @@ function redirects() {
|
||||
destination: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/wallet",
|
||||
destination: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/rpc",
|
||||
destination: "/dashboard/infrastructure/rpc-edge",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/storage",
|
||||
destination: "/dashboard/infrastructure/storage",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/smart-wallet",
|
||||
destination: "/account-abstraction",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/published",
|
||||
destination: "/dashboard/publish",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/wallets",
|
||||
destination: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/wallets/analytics",
|
||||
destination: "/dashboard/connect/analytics",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/wallets/embedded",
|
||||
destination: "/dashboard/connect/embedded-wallets",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/wallets/smart-wallet",
|
||||
destination: "/dashboard/connect/account-abstraction",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/wallets/connect",
|
||||
destination: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/infrastructure",
|
||||
destination: "/dashboard/settings/storage",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/infrastructure/storage",
|
||||
destination: "/dashboard/settings/storage",
|
||||
@@ -204,26 +183,6 @@ function redirects() {
|
||||
destination: "/chainlist",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/contracts",
|
||||
destination: "/dashboard/contracts/deploy",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/solidity-sdk",
|
||||
destination: "/build",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/connect-wallet",
|
||||
destination: "/connect",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/wallet-sdk",
|
||||
destination: "https://portal.thirdweb.com/wallet-sdk/latest",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/solutions/commerce",
|
||||
destination: "/solutions/loyalty",
|
||||
@@ -244,27 +203,12 @@ function redirects() {
|
||||
destination: "/optimism",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/payments/settings",
|
||||
destination: "/dashboard/connect/pay",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/payments/contracts",
|
||||
destination: "/dashboard/connect/pay",
|
||||
permanent: false,
|
||||
},
|
||||
// Redirecting as ambassadors lives in community now
|
||||
{
|
||||
source: "/ambassadors",
|
||||
destination: "/community/ambassadors",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/dashboard/connect/embedded-wallets",
|
||||
destination: "/dashboard/connect/in-app-wallets",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/embedded-wallets",
|
||||
destination: "/in-app-wallets",
|
||||
@@ -321,6 +265,7 @@ function redirects() {
|
||||
destination: "/account-abstraction",
|
||||
permanent: false,
|
||||
},
|
||||
...legacyDashboardToTeamRedirects,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
"use server";
|
||||
|
||||
import "server-only";
|
||||
import { API_SERVER_URL, getAbsoluteUrlFromPath } from "@/constants/env";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import type { ProductSKU } from "../lib/billing";
|
||||
|
||||
export type RedirectCheckoutOptions = {
|
||||
teamSlug: string;
|
||||
sku: ProductSKU;
|
||||
redirectPath?: string;
|
||||
metadata?: Record<string, string>;
|
||||
};
|
||||
export async function redirectToCheckout(
|
||||
options: RedirectCheckoutOptions,
|
||||
): Promise<{ status: number }> {
|
||||
if (!options.teamSlug) {
|
||||
return {
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${options.teamSlug}/checkout/create-link`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
sku: options.sku,
|
||||
redirectTo: getAbsoluteUrlFromPath(
|
||||
options.redirectPath ||
|
||||
`/team/${options.teamSlug}/~/settings/billing`,
|
||||
).toString(),
|
||||
metadata: options.metadata || {},
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
return {
|
||||
status: res.status,
|
||||
};
|
||||
}
|
||||
const json = await res.json();
|
||||
if (!json.result) {
|
||||
return {
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
|
||||
// redirect to the stripe checkout session
|
||||
redirect(json.result);
|
||||
}
|
||||
|
||||
export type BillingPortalOptions = {
|
||||
teamSlug: string | undefined;
|
||||
redirectPath?: string;
|
||||
};
|
||||
export async function redirectToBillingPortal(
|
||||
options: BillingPortalOptions,
|
||||
): Promise<{ status: number }> {
|
||||
if (!options.teamSlug) {
|
||||
return {
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
return {
|
||||
status: 401,
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${options.teamSlug}/checkout/create-session-link`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
redirectTo: getAbsoluteUrlFromPath(
|
||||
options.redirectPath ||
|
||||
`/team/${options.teamSlug}/~/settings/billing`,
|
||||
).toString(),
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
status: res.status,
|
||||
};
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (!json.result) {
|
||||
return {
|
||||
status: 500,
|
||||
};
|
||||
}
|
||||
|
||||
// redirect to the stripe billing portal
|
||||
redirect(json.result);
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export async function confirmEmailWithOTP(otp: string) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
errorMessage: "You are not authorized to perform this action",
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/account/confirmEmail`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
confirmationToken: otp,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
return {
|
||||
errorMessage: json.error.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
errorMessage: "Failed to confirm email",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
"use server";
|
||||
|
||||
import { getRawAccount } from "../../app/account/settings/getAccount";
|
||||
|
||||
export async function getRawAccountAction() {
|
||||
return getRawAccount();
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export async function joinTeamWaitlist(options: {
|
||||
teamSlug: string;
|
||||
// currently only 'nebula' is supported
|
||||
scope: "nebula";
|
||||
}) {
|
||||
const { teamSlug, scope } = options;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
errorMessage: "You are not authorized to perform this action",
|
||||
};
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
scope,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
return {
|
||||
errorMessage: "Failed to join waitlist",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
"use server";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
|
||||
export async function updateAccount(values: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
}) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
throw new Error("No Auth token");
|
||||
}
|
||||
|
||||
const res = await fetch(`${API_SERVER_URL}/v1/account`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(values),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
return {
|
||||
errorMessage: json.error.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
errorMessage: "Failed To Update Account",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { fetchAnalytics } from "data/analytics/fetch-analytics";
|
||||
|
||||
export interface WalletStats {
|
||||
date: string;
|
||||
uniqueWalletsConnected: number;
|
||||
totalConnections: number;
|
||||
walletType: string;
|
||||
}
|
||||
|
||||
export interface WalletUserStats {
|
||||
date: string;
|
||||
newUsers: number;
|
||||
returningUsers: number;
|
||||
totalUsers: number;
|
||||
}
|
||||
|
||||
export interface InAppWalletStats {
|
||||
date: string;
|
||||
authenticationMethod: string;
|
||||
uniqueWalletsConnected: number;
|
||||
}
|
||||
|
||||
export interface EcosystemWalletStats extends InAppWalletStats {}
|
||||
|
||||
export interface UserOpStats {
|
||||
date: string;
|
||||
successful: number;
|
||||
failed: number;
|
||||
sponsoredUsd: number;
|
||||
chainId?: string;
|
||||
}
|
||||
|
||||
interface AnalyticsQueryParams {
|
||||
clientId?: string;
|
||||
accountId?: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
}
|
||||
|
||||
function buildSearchParams(params: AnalyticsQueryParams): URLSearchParams {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (params.clientId) {
|
||||
searchParams.append("clientId", params.clientId);
|
||||
}
|
||||
if (params.accountId) {
|
||||
searchParams.append("accountId", params.accountId);
|
||||
}
|
||||
if (params.from) {
|
||||
searchParams.append("from", params.from.toISOString());
|
||||
}
|
||||
if (params.to) {
|
||||
searchParams.append("to", params.to.toISOString());
|
||||
}
|
||||
if (params.period) {
|
||||
searchParams.append("period", params.period);
|
||||
}
|
||||
return searchParams;
|
||||
}
|
||||
|
||||
export async function getWalletConnections(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<WalletStats[]> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(`v1/wallets?${searchParams.toString()}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch wallet connections");
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as WalletStats[];
|
||||
}
|
||||
|
||||
export async function getInAppWalletUsage(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<InAppWalletStats[]> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(
|
||||
`v1/wallets/in-app?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch in-app wallet usage");
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as InAppWalletStats[];
|
||||
}
|
||||
|
||||
export async function getUserOpUsage(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<UserOpStats[]> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(`v1/user-ops?${searchParams.toString()}`, {
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch user ops usage");
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as UserOpStats[];
|
||||
}
|
||||
|
||||
export async function getWalletUsers(
|
||||
params: AnalyticsQueryParams,
|
||||
): Promise<WalletUserStats[]> {
|
||||
const searchParams = buildSearchParams(params);
|
||||
const res = await fetchAnalytics(
|
||||
`v1/wallets/users?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
},
|
||||
);
|
||||
|
||||
if (res?.status !== 200) {
|
||||
console.error("Failed to fetch wallet user stats");
|
||||
return [];
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as WalletUserStats[];
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
import { API_SERVER_URL } from "../constants/env";
|
||||
import type { ProductSKU } from "../lib/billing";
|
||||
|
||||
type InvoiceLine = {
|
||||
// amount for this line item
|
||||
amount: number;
|
||||
// statement descriptor
|
||||
description: string | null;
|
||||
// the thirdweb product sku or null if it is not recognized
|
||||
thirdwebSku: ProductSKU | null;
|
||||
};
|
||||
|
||||
type Invoice = {
|
||||
// total amount excluding tax
|
||||
amount: number | null;
|
||||
// the ISO currency code (e.g. USD)
|
||||
currency: string;
|
||||
// the line items on the invoice
|
||||
lines: InvoiceLine[];
|
||||
};
|
||||
|
||||
export type TeamSubscription = {
|
||||
id: string;
|
||||
type: "PLAN" | "USAGE" | "PLAN_ADD_ON" | "PRODUCT";
|
||||
status:
|
||||
| "incomplete"
|
||||
| "incomplete_expired"
|
||||
| "trialing"
|
||||
| "active"
|
||||
| "past_due"
|
||||
| "canceled"
|
||||
| "unpaid"
|
||||
| "paused";
|
||||
currentPeriodStart: string;
|
||||
currentPeriodEnd: string;
|
||||
trialStart: string | null;
|
||||
trialEnd: string | null;
|
||||
upcomingInvoice: Invoice;
|
||||
};
|
||||
|
||||
export async function getTeamSubscriptions(slug: string) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamRes = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${slug}/subscriptions`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (teamRes.ok) {
|
||||
return (await teamRes.json())?.result as TeamSubscription[];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
import { getAuthToken } from "../../app/api/lib/getAuthToken";
|
||||
|
||||
export type Team = {
|
||||
id: string;
|
||||
@@ -12,22 +11,14 @@ export type Team = {
|
||||
deletedAt?: string;
|
||||
bannedAt?: string;
|
||||
image?: string;
|
||||
billingPlan: "pro" | "growth" | "free";
|
||||
billingStatus: "validPayment" | (string & {}); // what's the other value?
|
||||
billingEmail: string;
|
||||
// billingExternalId: string;
|
||||
// billingType: "STRIPE" | ??
|
||||
// billingCustomerPayload: ?? | null
|
||||
// paymentAttemptCount: number
|
||||
// trialPeriodEndedAt: string | null;
|
||||
billingPlan: "pro" | "growth" | "free" | "starter";
|
||||
billingStatus: "validPayment" | (string & {}) | null;
|
||||
billingEmail: string | null;
|
||||
growthTrialEligible: boolean | null;
|
||||
};
|
||||
|
||||
export async function getTeamBySlug(slug: string) {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
@@ -45,14 +36,9 @@ export async function getTeamBySlug(slug: string) {
|
||||
}
|
||||
|
||||
export async function getTeams() {
|
||||
const cookiesManager = await cookies();
|
||||
const activeAccount = cookiesManager.get(COOKIE_ACTIVE_ACCOUNT)?.value;
|
||||
const token = activeAccount
|
||||
? cookiesManager.get(COOKIE_PREFIX_TOKEN + activeAccount)?.value
|
||||
: null;
|
||||
|
||||
const token = await getAuthToken();
|
||||
if (!token) {
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(`${API_SERVER_URL}/v1/teams`, {
|
||||
@@ -63,5 +49,33 @@ export async function getTeams() {
|
||||
if (teamsRes.ok) {
|
||||
return (await teamsRes.json())?.result as Team[];
|
||||
}
|
||||
return [];
|
||||
return null;
|
||||
}
|
||||
|
||||
type TeamNebulWaitList = {
|
||||
onWaitlist: boolean;
|
||||
createdAt: null | string;
|
||||
};
|
||||
|
||||
export async function getTeamNebulaWaitList(teamSlug: string) {
|
||||
const token = await getAuthToken();
|
||||
|
||||
if (!token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const res = await fetch(
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/waitlist?scope=nebula`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (res.ok) {
|
||||
return (await res.json()).result as TeamNebulWaitList;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { setCookie } from "../../stores/SyncStoreToCookies";
|
||||
|
||||
export function DashboardTypeCookieSetter(props: {
|
||||
type: "team" | "old";
|
||||
}) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
setCookie("x-dashboard-type", props.type);
|
||||
}, [props.type]);
|
||||
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type BillingPortalOptions,
|
||||
type RedirectCheckoutOptions,
|
||||
redirectToBillingPortal,
|
||||
redirectToCheckout,
|
||||
} from "../actions/billing";
|
||||
import { Button, type ButtonProps } from "./ui/button";
|
||||
|
||||
type CheckoutButtonProps = RedirectCheckoutOptions & ButtonProps;
|
||||
export function CheckoutButton({
|
||||
onClick,
|
||||
teamSlug,
|
||||
sku,
|
||||
metadata,
|
||||
redirectPath,
|
||||
children,
|
||||
...restProps
|
||||
}: CheckoutButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
{...restProps}
|
||||
onClick={async (e) => {
|
||||
onClick?.(e);
|
||||
await redirectToCheckout({
|
||||
teamSlug,
|
||||
sku,
|
||||
metadata,
|
||||
redirectPath,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
type BillingPortalButtonProps = BillingPortalOptions & ButtonProps;
|
||||
export function BillingPortalButton({
|
||||
onClick,
|
||||
teamSlug,
|
||||
redirectPath,
|
||||
children,
|
||||
...restProps
|
||||
}: BillingPortalButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
{...restProps}
|
||||
onClick={async (e) => {
|
||||
onClick?.(e);
|
||||
await redirectToBillingPortal({
|
||||
teamSlug,
|
||||
redirectPath,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { getThirdwebClient } from "../../../constants/thirdweb.server";
|
||||
import { Button } from "../../ui/button";
|
||||
import { GradientAvatar } from "./GradientAvatar";
|
||||
|
||||
@@ -17,29 +18,74 @@ export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
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>
|
||||
|
||||
<BadgeContainer label="No Src, No id - Skeleton">
|
||||
<GradientAvatar id={undefined} src={undefined} className="size-6" />
|
||||
<GradientAvatar
|
||||
id={undefined}
|
||||
src={undefined}
|
||||
className="size-6"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Src, id=foo - Skeleton">
|
||||
<GradientAvatar id={"foo"} src={undefined} className="size-6" />
|
||||
<GradientAvatar
|
||||
id={"foo"}
|
||||
src={undefined}
|
||||
className="size-6"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src, id=foo - Gradient">
|
||||
<GradientAvatar id={"foo"} src={""} className="size-6" />
|
||||
<BadgeContainer label="Invalid Src, id=foo - Gradient">
|
||||
<GradientAvatar
|
||||
id={"foo"}
|
||||
src={""}
|
||||
className="size-6"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src, id=bar - Gradient">
|
||||
<GradientAvatar id={"bar"} src={""} className="size-6" />
|
||||
<BadgeContainer label="Invalid Src, id=bar - Gradient">
|
||||
<GradientAvatar
|
||||
id={"bar"}
|
||||
src={""}
|
||||
className="size-6"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Empty/Invalid Src, No id - default fallback">
|
||||
<GradientAvatar src="invalid-src" id={undefined} className="size-6" />
|
||||
<GradientAvatar
|
||||
src="invalid-src"
|
||||
id={undefined}
|
||||
className="size-6"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Valid Src, https">
|
||||
<GradientAvatar
|
||||
src="https://picsum.photos/200/300"
|
||||
id={undefined}
|
||||
className="size-6"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Valid Src, ipfs">
|
||||
<GradientAvatar
|
||||
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
|
||||
id={undefined}
|
||||
className="size-6"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<ToggleTest />
|
||||
@@ -74,7 +120,12 @@ function ToggleTest() {
|
||||
<p> Src+ID is: {data ? "set" : "not set"} </p>
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<GradientAvatar src={data?.src} id={data?.id} className="size-6" />
|
||||
<GradientAvatar
|
||||
src={data?.src}
|
||||
id={data?.id}
|
||||
className="size-6"
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
@@ -82,6 +133,7 @@ function ToggleTest() {
|
||||
className="size-6"
|
||||
src={data ? "invalid-src" : undefined}
|
||||
id={undefined}
|
||||
client={client}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { useMemo } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { resolveSchemeWithErrorHandler } from "../../../lib/resolveSchemeWithErrorHandler";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
const gradients = [
|
||||
@@ -32,6 +34,7 @@ export function GradientAvatar(props: {
|
||||
src: string | undefined;
|
||||
id: string | undefined;
|
||||
className: string;
|
||||
client: ThirdwebClient;
|
||||
}) {
|
||||
const gradient = useMemo(() => {
|
||||
if (!props.id) {
|
||||
@@ -40,9 +43,16 @@ export function GradientAvatar(props: {
|
||||
return getGradientForString(props.id);
|
||||
}, [props.id]);
|
||||
|
||||
const resolvedSrc = props.src
|
||||
? resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: props.src,
|
||||
})
|
||||
: props.src;
|
||||
|
||||
return (
|
||||
<Img
|
||||
src={props.src}
|
||||
src={resolvedSrc}
|
||||
className={cn("rounded-full", props.className)}
|
||||
fallback={
|
||||
gradient ? (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { DangerSettingCard } from "./DangerSettingCard";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/DangerSettingCard",
|
||||
title: "blocks/Cards/DangerSettingCard",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { SettingsCard } from "./SettingsCard";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/SettingsCard",
|
||||
title: "Blocks/Cards/SettingsCard",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
|
||||
@@ -51,7 +51,8 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
{props.links.map((link, i) => {
|
||||
if ("group" in link) {
|
||||
return (
|
||||
<div className={cn({ "mt-6": i !== 0 })}>
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: items won't be reordered
|
||||
<div className={cn({ "mt-6": i !== 0 })} key={i}>
|
||||
<p className={cn("px-3 py-2 font-medium text-foreground")}>
|
||||
{link.group}
|
||||
</p>
|
||||
@@ -63,7 +64,8 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
const isExternal = link.href.startsWith("http");
|
||||
return (
|
||||
<NavLink
|
||||
key={link.href}
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: items won't be reordered
|
||||
key={i}
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-muted"
|
||||
activeClassName="text-foreground"
|
||||
|
||||
@@ -19,7 +19,7 @@ export function AppFooter(props: AppFooterProps) {
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
"w-full border-border border-t bg-white py-6 md:py-8 dark:bg-muted/50",
|
||||
"w-full border-border border-t bg-muted/50 py-6 md:py-8",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
"use client";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import type React from "react";
|
||||
import { type Dispatch, type SetStateAction, useMemo } from "react";
|
||||
import { TabButtons } from "../ui/tabs";
|
||||
|
||||
export type CodeEnvironment =
|
||||
| "javascript"
|
||||
| "typescript"
|
||||
| "react"
|
||||
| "react-native"
|
||||
| "unity";
|
||||
|
||||
type SupportedEnvironment = {
|
||||
environment: CodeEnvironment;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type CodeSnippet = Partial<Record<CodeEnvironment, string>>;
|
||||
|
||||
const Environments: SupportedEnvironment[] = [
|
||||
{
|
||||
environment: "javascript",
|
||||
title: "JavaScript",
|
||||
},
|
||||
{
|
||||
environment: "typescript",
|
||||
title: "TypeScript",
|
||||
},
|
||||
{
|
||||
environment: "react",
|
||||
title: "React",
|
||||
},
|
||||
{
|
||||
environment: "react-native",
|
||||
title: "React Native",
|
||||
},
|
||||
{
|
||||
environment: "unity",
|
||||
title: "Unity",
|
||||
},
|
||||
];
|
||||
|
||||
interface CodeSegmentProps {
|
||||
snippet: CodeSnippet;
|
||||
environment: CodeEnvironment;
|
||||
setEnvironment:
|
||||
| Dispatch<SetStateAction<CodeEnvironment>>
|
||||
| ((language: CodeEnvironment) => void);
|
||||
isInstallCommand?: boolean;
|
||||
hideTabs?: boolean;
|
||||
onlyTabs?: boolean;
|
||||
}
|
||||
|
||||
export const CodeSegment: React.FC<CodeSegmentProps> = ({
|
||||
snippet,
|
||||
environment,
|
||||
setEnvironment,
|
||||
isInstallCommand,
|
||||
hideTabs,
|
||||
onlyTabs,
|
||||
}) => {
|
||||
const activeEnvironment: CodeEnvironment = useMemo(() => {
|
||||
return (
|
||||
snippet[environment] ? environment : Object.keys(snippet)[0]
|
||||
) as CodeEnvironment;
|
||||
}, [environment, snippet]);
|
||||
|
||||
const activeSnippet = useMemo(() => {
|
||||
return snippet[activeEnvironment];
|
||||
}, [activeEnvironment, snippet]);
|
||||
|
||||
const lines = useMemo(
|
||||
() => (activeSnippet ? activeSnippet.split("\n") : []),
|
||||
[activeSnippet],
|
||||
);
|
||||
|
||||
const code = lines.join("\n").trim();
|
||||
|
||||
const environments = Environments.filter(
|
||||
(env) =>
|
||||
Object.keys(snippet).includes(env.environment) &&
|
||||
snippet[env.environment],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"flex flex-col overflow-hidden rounded-lg border border-border"
|
||||
}
|
||||
>
|
||||
{!hideTabs && (
|
||||
<TabButtons
|
||||
tabs={environments.map((env) => ({
|
||||
label: env.title,
|
||||
onClick: () => setEnvironment(env.environment),
|
||||
isActive: activeEnvironment === env.environment,
|
||||
name: env.title,
|
||||
isEnabled: true,
|
||||
}))}
|
||||
tabClassName="text-sm gap-2 !text-sm"
|
||||
tabIconClassName="size-4"
|
||||
tabContainerClassName="px-3 pt-1.5 gap-0.5"
|
||||
hideBottomLine={!!onlyTabs}
|
||||
/>
|
||||
)}
|
||||
|
||||
{onlyTabs ? null : (
|
||||
<>
|
||||
<CodeClient
|
||||
code={code}
|
||||
loadingClassName="min-h-[450px] rounded-none border-none"
|
||||
className="rounded-none border-none"
|
||||
lang={
|
||||
isInstallCommand
|
||||
? activeEnvironment === "react-native"
|
||||
? "tsx"
|
||||
: "bash"
|
||||
: activeEnvironment === "react" ||
|
||||
activeEnvironment === "react-native"
|
||||
? "tsx"
|
||||
: activeEnvironment === "unity"
|
||||
? "cpp"
|
||||
: activeEnvironment
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { type CodeEnvironment, CodeSegment } from "./code-segment.client";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/CodeSegment",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
type Mode = "default" | "no-tabs" | "only-tabs";
|
||||
|
||||
function Story() {
|
||||
const [mode, setMode] = useState<Mode>("default");
|
||||
const modes: Mode[] = ["default", "no-tabs", "only-tabs"];
|
||||
|
||||
return (
|
||||
<div className="container flex max-w-[700px] flex-col gap-10 py-10">
|
||||
<Select
|
||||
value={mode}
|
||||
onValueChange={(v) => {
|
||||
setMode(v as Mode);
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectGroup>
|
||||
{modes.map((item) => {
|
||||
return (
|
||||
<SelectItem key={item} value={item}>
|
||||
{item}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectGroup>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Variant envsToShow={["javascript"]} storyLabel="1 env" mode={mode} />
|
||||
|
||||
<Variant
|
||||
envsToShow={["javascript", "typescript"]}
|
||||
storyLabel="2 envs"
|
||||
mode={mode}
|
||||
/>
|
||||
|
||||
<Variant
|
||||
envsToShow={[
|
||||
"javascript",
|
||||
"typescript",
|
||||
"react",
|
||||
"react-native",
|
||||
"unity",
|
||||
]}
|
||||
storyLabel="5 envs"
|
||||
mode={mode}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
envsToShow: CodeEnvironment[];
|
||||
storyLabel: string;
|
||||
mode: Mode;
|
||||
}) {
|
||||
const [env, setEnv] = useState<CodeEnvironment>(
|
||||
props.envsToShow[0] || "javascript",
|
||||
);
|
||||
return (
|
||||
<BadgeContainer label={props.storyLabel}>
|
||||
<CodeSegment
|
||||
snippet={{
|
||||
javascript: props.envsToShow.includes("javascript")
|
||||
? jsCode
|
||||
: undefined,
|
||||
react: props.envsToShow.includes("react") ? jsxCode : undefined,
|
||||
unity: props.envsToShow.includes("unity") ? unityCode : undefined,
|
||||
"react-native": props.envsToShow.includes("react-native")
|
||||
? jsxCode
|
||||
: undefined,
|
||||
typescript: props.envsToShow.includes("typescript")
|
||||
? tsCode
|
||||
: undefined,
|
||||
}}
|
||||
environment={env}
|
||||
setEnvironment={setEnv}
|
||||
hideTabs={props.mode === "no-tabs"}
|
||||
onlyTabs={props.mode === "only-tabs"}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
}
|
||||
|
||||
const jsCode = `\
|
||||
import { getContract } from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { getOwnedNFTs } from "thirdweb/extensions/erc1155";
|
||||
|
||||
const contract = getContract({
|
||||
client,
|
||||
address: "0x1234...",
|
||||
chain: sepolia,
|
||||
});
|
||||
`;
|
||||
|
||||
const jsxCode = `\
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
|
||||
function Main() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<App />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}`;
|
||||
|
||||
const unityCode = `\
|
||||
using Thirdweb;
|
||||
|
||||
// Reference the SDK
|
||||
var sdk = ThirdwebManager.Instance.SDK;
|
||||
|
||||
// Create data
|
||||
NFTMetadata meta = new NFTMetadata()
|
||||
{
|
||||
name = "Unity NFT",
|
||||
description = "Minted From Unity",
|
||||
image = "ipfs://QmbpciV7R5SSPb6aT9kEBAxoYoXBUsStJkMpxzymV4ZcVc",
|
||||
};
|
||||
string metaJson = Newtonsoft.Json.JsonConvert.SerializeObject(meta);
|
||||
|
||||
// Upload raw text or from a file path
|
||||
var response = await ThirdwebManager.Instance.SDK.storage.UploadText(metaJson);`;
|
||||
|
||||
const tsCode = `\
|
||||
type User = {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
function logUser(user: User) {
|
||||
console.log(user)
|
||||
}
|
||||
`;
|
||||
@@ -1,46 +0,0 @@
|
||||
import { Highlight } from "prism-react-renderer";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { CopyButton } from "./CopyButton";
|
||||
import { Card } from "./card";
|
||||
|
||||
interface CodeBlockProps {
|
||||
code: string;
|
||||
language?: string;
|
||||
className?: string;
|
||||
canCopy?: boolean;
|
||||
}
|
||||
|
||||
export const CodeBlock = ({
|
||||
code,
|
||||
language = "sh",
|
||||
canCopy = true,
|
||||
}: CodeBlockProps) => {
|
||||
return (
|
||||
<Highlight code={code} language={language}>
|
||||
{({ className, tokens, getLineProps, getTokenProps }) => (
|
||||
<Card className={className}>
|
||||
{canCopy && (
|
||||
<CopyButton text={code} className="float-right m-2 p-2" />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"block overflow-auto whitespace-pre-wrap p-4 font-mono text-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{tokens.map((line, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: index IS the key here
|
||||
<div {...getLineProps({ line, key: i })} key={i}>
|
||||
{line.map((token, key) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: index IS the key here
|
||||
<span {...getTokenProps({ token, key })} key={key} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</Highlight>
|
||||
);
|
||||
};
|
||||
@@ -50,7 +50,7 @@ export function DatePickerWithRange(props: {
|
||||
id="date"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"justify-start gap-2 bg-card text-left font-normal",
|
||||
"justify-start gap-2 text-left font-normal",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Button } from "./button";
|
||||
|
||||
export function PlainTextCodeBlock(props: {
|
||||
code: string;
|
||||
copyButtonClassName?: string;
|
||||
}) {
|
||||
const { hasCopied, onCopy } = useClipboard(props.code);
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<code className="block whitespace-pre rounded-lg border border-border bg-muted/50 p-4">
|
||||
{props.code}
|
||||
</code>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onCopy}
|
||||
className={cn(
|
||||
"absolute top-3.5 right-3.5 h-auto p-2",
|
||||
props.copyButtonClassName,
|
||||
)}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="size-3 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="size-3 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
export function RenderCode(props: {
|
||||
code: string;
|
||||
html: string;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative max-w-full overflow-hidden rounded-lg border border-border bg-background",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<ScrollShadow
|
||||
scrollableClassName={cn("p-4", props.scrollableClassName)}
|
||||
className="text-xs md:text-sm [&_*]:leading-relaxed"
|
||||
shadowColor="hsl(var(--muted))"
|
||||
>
|
||||
<div
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: we know what we're doing here
|
||||
dangerouslySetInnerHTML={{ __html: props.html }}
|
||||
/>
|
||||
</ScrollShadow>
|
||||
<CopyButton
|
||||
text={props.code}
|
||||
iconClassName="size-3"
|
||||
className="absolute top-4 right-4 z-10 border border-border bg-background p-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CopyButton(props: {
|
||||
text: string;
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
return (
|
||||
<ToolTipLabel label="Copy">
|
||||
<Button
|
||||
variant="ghost"
|
||||
aria-label="Copy"
|
||||
className={cn("h-auto w-auto p-1", props.className)}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(props.text);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
}}
|
||||
>
|
||||
{isCopied ? (
|
||||
<CheckIcon
|
||||
className={cn("size-4 text-green-500", props.iconClassName)}
|
||||
/>
|
||||
) : (
|
||||
<CopyIcon
|
||||
className={cn("size-4 text-muted-foreground", props.iconClassName)}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import type { BundledLanguage } from "shiki";
|
||||
import { Spinner } from "../../ui/Spinner/Spinner";
|
||||
import { RenderCode } from "./RenderCode";
|
||||
import { getCodeHtml } from "./getCodeHtml";
|
||||
|
||||
export type CodeProps = {
|
||||
code: string;
|
||||
lang: BundledLanguage;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
loadingClassName?: string;
|
||||
keepPreviousDataOnCodeChange?: boolean;
|
||||
};
|
||||
|
||||
export const CodeClient: React.FC<CodeProps> = ({
|
||||
code,
|
||||
lang,
|
||||
className,
|
||||
scrollableClassName,
|
||||
loadingClassName,
|
||||
keepPreviousDataOnCodeChange = false,
|
||||
}) => {
|
||||
const codeQuery = useQuery({
|
||||
queryKey: ["html", code],
|
||||
queryFn: () => getCodeHtml(code, lang),
|
||||
placeholderData: keepPreviousDataOnCodeChange
|
||||
? keepPreviousData
|
||||
: undefined,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
if (!codeQuery.data) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-h-[200px] items-center justify-center rounded-lg border border-border",
|
||||
loadingClassName,
|
||||
)}
|
||||
>
|
||||
<Spinner className="size-8" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderCode
|
||||
code={codeQuery.data.formattedCode}
|
||||
html={codeQuery.data.html}
|
||||
className={className}
|
||||
scrollableClassName={scrollableClassName}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,18 @@
|
||||
import type { BundledLanguage } from "shiki";
|
||||
import { RenderCode } from "./RenderCode";
|
||||
import { getCodeHtml } from "./getCodeHtml";
|
||||
|
||||
export type CodeProps = {
|
||||
code: string;
|
||||
lang: BundledLanguage;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const CodeServer: React.FC<CodeProps> = async ({
|
||||
code,
|
||||
lang,
|
||||
className,
|
||||
}) => {
|
||||
const { html, formattedCode } = await getCodeHtml(code, lang);
|
||||
return <RenderCode code={formattedCode} html={html} className={className} />;
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { CodeClient } from "./code.client";
|
||||
|
||||
const meta = {
|
||||
title: "code/lang",
|
||||
component: Component,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const tsCode = `\
|
||||
type User = {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
function logUser(user: User) {
|
||||
console.log(user)
|
||||
}
|
||||
`;
|
||||
|
||||
const jsCode = `\
|
||||
import { getContract } from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { getOwnedNFTs } from "thirdweb/extensions/erc1155";
|
||||
|
||||
const contract = getContract({
|
||||
client,
|
||||
address: "0x1234...",
|
||||
chain: sepolia,
|
||||
});
|
||||
`;
|
||||
|
||||
const jsxCode = `\
|
||||
import { ThirdwebProvider } from "thirdweb/react";
|
||||
|
||||
function Main() {
|
||||
return (
|
||||
<ThirdwebProvider>
|
||||
<App />
|
||||
</ThirdwebProvider>
|
||||
);
|
||||
}`;
|
||||
|
||||
const tsxCode = `\
|
||||
type User = {
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
function UserInfo(props: { user: User }) {
|
||||
return <div> {props.user.name} </div>
|
||||
}
|
||||
`;
|
||||
|
||||
const tsxCodeWithFormError = `\
|
||||
// This piece of code has invalid syntax and can't be formatted by prettier
|
||||
// this should not crash the page - and be rendered as is without formatting
|
||||
// the format error is logged in console
|
||||
|
||||
type User = // missing { here
|
||||
name: string;
|
||||
age: number;
|
||||
}
|
||||
|
||||
function UserInfo(props: { user: User }) {
|
||||
return <div> {props.user.name} </div>
|
||||
}
|
||||
`;
|
||||
|
||||
const bashCode = "pnpm i thirdweb";
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="ts">
|
||||
<CodeClient code={tsCode} lang="ts" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="js">
|
||||
<CodeClient code={jsCode} lang="js" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="bash">
|
||||
<CodeClient code={bashCode} lang="bash" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="jsx">
|
||||
<CodeClient code={jsxCode} lang="jsx" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="tsx">
|
||||
<CodeClient code={tsxCode} lang="tsx" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="tsx">
|
||||
<CodeClient code={tsxCodeWithFormError} lang="tsx" />
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as parserBabel from "prettier/plugins/babel";
|
||||
import * as estree from "prettier/plugins/estree";
|
||||
import { format } from "prettier/standalone";
|
||||
import { type BundledLanguage, codeToHtml } from "shiki";
|
||||
|
||||
function isPrettierSupportedLang(lang: BundledLanguage) {
|
||||
return (
|
||||
lang === "js" ||
|
||||
lang === "jsx" ||
|
||||
lang === "ts" ||
|
||||
lang === "tsx" ||
|
||||
lang === "javascript" ||
|
||||
lang === "typescript" ||
|
||||
lang === "css" ||
|
||||
lang === "json"
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCodeHtml(code: string, lang: BundledLanguage) {
|
||||
const formattedCode = isPrettierSupportedLang(lang)
|
||||
? await format(code, {
|
||||
parser: "babel-ts",
|
||||
plugins: [parserBabel, estree],
|
||||
printWidth: 60,
|
||||
}).catch((e) => {
|
||||
console.error(e);
|
||||
console.error("Failed to format code");
|
||||
console.log({
|
||||
code,
|
||||
lang,
|
||||
});
|
||||
return code;
|
||||
})
|
||||
: code;
|
||||
|
||||
const html = await codeToHtml(formattedCode, {
|
||||
lang: lang,
|
||||
themes: {
|
||||
light: "github-light",
|
||||
dark: "github-dark-default",
|
||||
},
|
||||
});
|
||||
|
||||
return { html, formattedCode };
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer, mobileViewport } from "stories/utils";
|
||||
import { PlainTextCodeBlock } from "./plaintext-code";
|
||||
|
||||
const meta = {
|
||||
title: "code/plaintext",
|
||||
component: Component,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Component>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
const jsCode = `\
|
||||
import { getContract } from "thirdweb";
|
||||
import { sepolia } from "thirdweb/chains";
|
||||
import { getOwnedNFTs } from "thirdweb/extensions/erc1155";
|
||||
|
||||
const contract = getContract({
|
||||
client,
|
||||
address: "0x1234...",
|
||||
chain: sepolia,
|
||||
});
|
||||
`;
|
||||
|
||||
const overflowText =
|
||||
"123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz123456789abcdefghijklmnopqrstuvwxyz";
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="container flex max-w-[600px] flex-col gap-10 py-10">
|
||||
<BadgeContainer label="large chunk of code">
|
||||
<PlainTextCodeBlock code={jsCode} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="overflow test">
|
||||
<PlainTextCodeBlock code={overflowText} />
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { ScrollShadow } from "../ScrollShadow/ScrollShadow";
|
||||
import { Button } from "../button";
|
||||
|
||||
export function PlainTextCodeBlock(props: {
|
||||
code: string;
|
||||
copyButtonClassName?: string;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
codeClassName?: string;
|
||||
}) {
|
||||
const { hasCopied, onCopy } = useClipboard(props.code);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"group relative max-w-full overflow-hidden rounded-lg border border-border bg-background",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<ScrollShadow
|
||||
scrollableClassName={cn("p-4", props.scrollableClassName)}
|
||||
className="text-xs md:text-sm [&_*]:leading-relaxed"
|
||||
shadowColor="hsl(var(--muted))"
|
||||
>
|
||||
<code className={cn("block whitespace-pre", props.codeClassName)}>
|
||||
{props.code}
|
||||
</code>
|
||||
</ScrollShadow>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={onCopy}
|
||||
className={cn(
|
||||
"absolute top-3.5 right-3.5 h-auto bg-background p-2",
|
||||
props.copyButtonClassName,
|
||||
)}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="size-3 text-green-500" />
|
||||
) : (
|
||||
<CopyIcon className="size-3 text-muted-foreground" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -35,40 +35,43 @@ const DialogContent = React.forwardRef<
|
||||
dialogOverlayClassName?: string;
|
||||
dialogCloseClassName?: string;
|
||||
}
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<DialogPortal>
|
||||
<DialogOverlay className={props.dialogOverlayClassName} />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
|
||||
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
|
||||
"right-0 bottom-0 left-0",
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
// on desktop - vertically and horizontally center the dialog, animate - zoom and fade in
|
||||
"md:top-[50%] md:bottom-auto md:left-[50%] md:translate-x-[-50%] md:translate-y-[-50%] ",
|
||||
"md:data-[state=closed]:zoom-out-95 md:data-[state=open]:zoom-in-95 ",
|
||||
"md:data-[state=closed]:slide-out-to-left-1/2 md:data-[state=closed]:slide-out-to-top-[48%] ",
|
||||
"md:data-[state=open]:slide-in-from-left-1/2 md:data-[state=open]:slide-in-from-top-[48%] ",
|
||||
"data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
>(({ className, children, ...props }, ref) => {
|
||||
const { dialogOverlayClassName, dialogCloseClassName, ...restProps } = props;
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogOverlay className={dialogOverlayClassName} />
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
||||
props.dialogCloseClassName,
|
||||
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
|
||||
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
|
||||
"right-0 bottom-0 left-0",
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
// on desktop - vertically and horizontally center the dialog, animate - zoom and fade in
|
||||
"md:top-[50%] md:bottom-auto md:left-[50%] md:translate-x-[-50%] md:translate-y-[-50%] ",
|
||||
"md:data-[state=closed]:zoom-out-95 md:data-[state=open]:zoom-in-95 ",
|
||||
"md:data-[state=closed]:slide-out-to-left-1/2 md:data-[state=closed]:slide-out-to-top-[48%] ",
|
||||
"md:data-[state=open]:slide-in-from-left-1/2 md:data-[state=open]:slide-in-from-top-[48%] ",
|
||||
"data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
{...restProps}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
));
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
"absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
||||
dialogCloseClassName,
|
||||
)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
);
|
||||
});
|
||||
DialogContent.displayName = DialogPrimitive.Content.displayName;
|
||||
|
||||
const DialogHeader = ({
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
|
||||
const meta = {
|
||||
title: "Shadcn/select",
|
||||
title: "Shadcn/Select",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import Link from "next/link";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Badge } from "./badge";
|
||||
|
||||
const meta = {
|
||||
title: "Shadcn/table",
|
||||
title: "Shadcn/Table",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
@@ -41,6 +43,10 @@ function Component() {
|
||||
<TableDemo />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Clickable Row">
|
||||
<TableDemo linkBox />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="With Footer">
|
||||
<TableDemo footer />
|
||||
</BadgeContainer>
|
||||
@@ -90,6 +96,7 @@ const invoices: Invoice[] = [
|
||||
|
||||
function TableDemo(props: {
|
||||
footer?: boolean;
|
||||
linkBox?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<TableContainer>
|
||||
@@ -104,8 +111,21 @@ function TableDemo(props: {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{invoices.map((invoice) => (
|
||||
<TableRow key={invoice.invoice}>
|
||||
<TableCell className="font-medium">{invoice.invoice}</TableCell>
|
||||
<TableRow
|
||||
key={invoice.invoice}
|
||||
linkBox={props.linkBox}
|
||||
className={cn(props.linkBox && "cursor-pointer hover:bg-muted")}
|
||||
>
|
||||
<TableCell className="font-medium">
|
||||
<Link
|
||||
href={`/invoices/${invoice.invoice}`}
|
||||
className={cn(
|
||||
props.linkBox && "before:absolute before:inset-0",
|
||||
)}
|
||||
>
|
||||
{invoice.invoice}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge>{invoice.paymentStatus}</Badge>
|
||||
</TableCell>
|
||||
|
||||
@@ -63,12 +63,19 @@ TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
React.HTMLAttributes<HTMLTableRowElement> & {
|
||||
/**
|
||||
* Contain the absolutely position elements inside the row with position:relative + transform:translate(0)
|
||||
* transform:translate(0) is required because position:relative on tr element does not work on webkit
|
||||
*/
|
||||
linkBox?: boolean;
|
||||
}
|
||||
>(({ className, linkBox, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-border border-b last:border-0 data-[state=selected]:bg-muted",
|
||||
linkBox && "relative translate-x-0 translate-y-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -87,6 +87,8 @@ export function TabButtons(props: {
|
||||
tabContainerClassName?: string;
|
||||
containerClassName?: string;
|
||||
shadowColor?: string;
|
||||
tabIconClassName?: string;
|
||||
hideBottomLine?: boolean;
|
||||
}) {
|
||||
const { containerRef, lineRef, activeTabRef } =
|
||||
useUnderline<HTMLButtonElement>();
|
||||
@@ -94,7 +96,9 @@ export function TabButtons(props: {
|
||||
return (
|
||||
<div className={cn("relative", props.containerClassName)}>
|
||||
{/* Bottom line */}
|
||||
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
|
||||
{!props.hideBottomLine && (
|
||||
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
|
||||
)}
|
||||
|
||||
<ScrollShadow
|
||||
scrollableClassName="pb-[8px] relative"
|
||||
@@ -120,7 +124,9 @@ export function TabButtons(props: {
|
||||
)}
|
||||
onClick={tab.isEnabled ? tab.onClick : undefined}
|
||||
>
|
||||
{tab.icon && <tab.icon className="size-6" />}
|
||||
{tab.icon && (
|
||||
<tab.icon className={cn("size-6", props.tabIconClassName)} />
|
||||
)}
|
||||
{tab.name}
|
||||
</Button>
|
||||
);
|
||||
@@ -197,6 +203,7 @@ export function TabPathLinks(props: {
|
||||
isActive: l.exactMatch
|
||||
? pathname === l.path
|
||||
: pathname.startsWith(l.path),
|
||||
isDisabled: l.isDisabled,
|
||||
}))}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,8 @@ export function isLoginRequired(pathname: string) {
|
||||
// remove '/' in front and then split by '/'
|
||||
const paths = pathname.slice(1).split("/");
|
||||
|
||||
// /dashboard, /team, /cli
|
||||
if (paths[0] === "dashboard" || paths[0] === "team" || paths[0] === "cli") {
|
||||
// /team, /cli
|
||||
if (paths[0] === "team" || paths[0] === "cli") {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,3 +33,16 @@ export const THIRDWEB_ENGINE_URL = process.env.THIRDWEB_ENGINE_URL;
|
||||
export const THIRDWEB_ACCESS_TOKEN = process.env.THIRDWEB_ACCESS_TOKEN;
|
||||
// Comma-separated list of chain IDs to disable faucet for.
|
||||
export const DISABLE_FAUCET_CHAIN_IDS = process.env.DISABLE_FAUCET_CHAIN_IDS;
|
||||
|
||||
export function getAbsoluteUrlFromPath(path: string) {
|
||||
const url = new URL(
|
||||
isProd
|
||||
? "https://thirdweb.com"
|
||||
: (process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL
|
||||
? `https://${process.env.NEXT_PUBLIC_VERCEL_BRANCH_URL}`
|
||||
: "http://localhost:3000") || "https://thirdweb-dev.com",
|
||||
);
|
||||
|
||||
url.pathname = path;
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
IPFS_GATEWAY_URL,
|
||||
} from "@/constants/env";
|
||||
import {
|
||||
THIRDWEB_BUNDLER_DOMAIN,
|
||||
THIRDWEB_INAPP_WALLET_DOMAIN,
|
||||
THIRDWEB_PAY_DOMAIN,
|
||||
THIRDWEB_RPC_DOMAIN,
|
||||
@@ -11,7 +12,13 @@ import {
|
||||
THIRDWEB_STORAGE_DOMAIN,
|
||||
} from "constants/urls";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { setThirdwebDomains } from "thirdweb/utils";
|
||||
import { populateEip712Transaction } from "thirdweb/transaction";
|
||||
import {
|
||||
getTransactionDecorator,
|
||||
setThirdwebDomains,
|
||||
setTransactionDecorator,
|
||||
} from "thirdweb/utils";
|
||||
import { getZkPaymasterData } from "thirdweb/wallets/smart";
|
||||
import { getVercelEnv } from "../../lib/vercel-utils";
|
||||
|
||||
// returns a thirdweb client with optional JWT passed in
|
||||
@@ -24,8 +31,43 @@ export function getThirdwebClient(jwt?: string) {
|
||||
pay: THIRDWEB_PAY_DOMAIN,
|
||||
storage: THIRDWEB_STORAGE_DOMAIN,
|
||||
social: THIRDWEB_SOCIAL_API_DOMAIN,
|
||||
bundler: THIRDWEB_BUNDLER_DOMAIN,
|
||||
});
|
||||
}
|
||||
|
||||
if (!getTransactionDecorator()) {
|
||||
setTransactionDecorator(async ({ account, transaction }) => {
|
||||
// special override for sophon testnet (zk chain)
|
||||
// sophon only allows transactions through their paymaster
|
||||
// so always use eip712 tx + paymaster
|
||||
if (transaction.chain.id === 531050104) {
|
||||
const serializedTx = await populateEip712Transaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
const pmData = await getZkPaymasterData({
|
||||
options: {
|
||||
client: transaction.client,
|
||||
chain: transaction.chain,
|
||||
},
|
||||
transaction: serializedTx,
|
||||
});
|
||||
return {
|
||||
account,
|
||||
transaction: {
|
||||
...transaction,
|
||||
eip712: {
|
||||
...transaction.eip712,
|
||||
paymaster: pmData.paymaster,
|
||||
paymasterInput: pmData.paymasterInput,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return { account, transaction };
|
||||
});
|
||||
}
|
||||
|
||||
return createThirdwebClient({
|
||||
secretKey: jwt ? jwt : DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
// keep in line with product SKUs in the backend
|
||||
export type ProductSKU =
|
||||
| "plan:starter"
|
||||
| "plan:growth"
|
||||
| "plan:custom"
|
||||
| "product:ecosystem_wallets"
|
||||
| "product:engine_standard"
|
||||
| "product:engine_premium"
|
||||
| "usage:storage"
|
||||
| "usage:in_app_wallet"
|
||||
| "usage:aa_sponsorship"
|
||||
| "usage:aa_sponsorship_op_grant"
|
||||
| null;
|
||||
@@ -6,7 +6,7 @@
|
||||
:root,
|
||||
[data-theme="light"] {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 98%;
|
||||
--background: 0 0% 100%;
|
||||
--popover: 0 0% 100%;
|
||||
--card: 0 0% 100%;
|
||||
--secondary: 0 0% 92%;
|
||||
@@ -46,7 +46,7 @@
|
||||
[data-theme="dark"] {
|
||||
/* bg - neutral */
|
||||
--background: 0 0% 0%;
|
||||
--card: 0 0% 5%;
|
||||
--card: 0 0% 0%;
|
||||
--popover: 0 0% 0%;
|
||||
--secondary: 0 0% 10%;
|
||||
--muted: 0 0% 9%;
|
||||
@@ -156,3 +156,13 @@ input:-webkit-autofill:active {
|
||||
/* Revert background color */
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
.dark .shiki,
|
||||
.dark .shiki span {
|
||||
color: var(--shiki-dark) !important;
|
||||
background-color: var(--background) !important;
|
||||
/* Optional, if you also want font styles */
|
||||
font-style: var(--shiki-dark-font-style) !important;
|
||||
font-weight: var(--shiki-dark-font-weight) !important;
|
||||
text-decoration: var(--shiki-dark-text-decoration) !important;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useStore } from "@/lib/reactive";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { getSDKTheme } from "app/components/sdk-component-theme";
|
||||
import { CustomChainRenderer } from "components/selects/CustomChainRenderer";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
@@ -15,7 +16,6 @@ import { usePathname } from "next/navigation";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { Chain } from "thirdweb";
|
||||
import {
|
||||
AutoConnect,
|
||||
ConnectButton,
|
||||
type NetworkSelectorProps,
|
||||
useConnectModal,
|
||||
@@ -36,6 +36,8 @@ export const CustomConnectWallet = (props: {
|
||||
connectButtonClassName?: string;
|
||||
signInLinkButtonClassName?: string;
|
||||
detailsButtonClassName?: string;
|
||||
loadingButtonClassName?: string;
|
||||
chain?: Chain;
|
||||
}) => {
|
||||
const thirdwebClient = useThirdwebClient();
|
||||
const loginRequired =
|
||||
@@ -123,11 +125,14 @@ export const CustomConnectWallet = (props: {
|
||||
if (isPending) {
|
||||
return (
|
||||
<>
|
||||
<div className="flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted",
|
||||
props.loadingButtonClassName,
|
||||
)}
|
||||
>
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{/* need auto connect here so that we actually connect */}
|
||||
<AutoConnect client={thirdwebClient} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -147,8 +152,6 @@ export const CustomConnectWallet = (props: {
|
||||
Sign In
|
||||
</Link>
|
||||
</Button>
|
||||
{/* need auto connect here so that we actually connect */}
|
||||
<AutoConnect client={thirdwebClient} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -209,6 +212,7 @@ export const CustomConnectWallet = (props: {
|
||||
},
|
||||
},
|
||||
}}
|
||||
chain={props.chain}
|
||||
/>
|
||||
|
||||
<LazyConfigureNetworkModal
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { UserOpStats } from "@/api/analytics";
|
||||
import type { Team } from "@/api/team";
|
||||
import {
|
||||
type Query,
|
||||
useMutation,
|
||||
@@ -28,9 +30,6 @@ export const accountPlan = {
|
||||
enterprise: "enterprise",
|
||||
} as const;
|
||||
|
||||
export type AccountStatus = (typeof accountStatus)[keyof typeof accountStatus];
|
||||
export type AccountPlan = (typeof accountPlan)[keyof typeof accountPlan];
|
||||
|
||||
export type AuthorizedWallet = {
|
||||
id: string;
|
||||
accountId: string;
|
||||
@@ -46,30 +45,17 @@ export type Account = {
|
||||
id: string;
|
||||
isStaff: boolean;
|
||||
creatorWalletAddress: string;
|
||||
status: AccountStatus;
|
||||
plan: AccountPlan;
|
||||
name?: string;
|
||||
email?: string;
|
||||
advancedEnabled: boolean;
|
||||
currentBillingPeriodStartsAt: string;
|
||||
currentBillingPeriodEndsAt: string;
|
||||
emailConfirmedAt?: string;
|
||||
unconfirmedEmail?: string;
|
||||
trialPeriodEndedAt?: string;
|
||||
emailConfirmationWalletAddress?: string;
|
||||
stripePaymentActionUrl?: string;
|
||||
onboardSkipped?: boolean;
|
||||
paymentAttemptCount?: number;
|
||||
notificationPreferences?: {
|
||||
billing: "email" | "none";
|
||||
updates: "email" | "none";
|
||||
};
|
||||
recurringPaymentFailures: {
|
||||
subscriptionId: string;
|
||||
subscriptionDescription: string;
|
||||
paymentFailureCode: string;
|
||||
serviceCutoffDate: string;
|
||||
}[];
|
||||
// TODO - add image URL
|
||||
};
|
||||
|
||||
@@ -193,33 +179,16 @@ export interface UpdateKeyInput {
|
||||
redirectUrls: string[];
|
||||
}
|
||||
|
||||
interface UsageBundler {
|
||||
chainId: number;
|
||||
sumTransactionFee: string;
|
||||
}
|
||||
|
||||
interface UsageStorage {
|
||||
sumFileSizeBytes: number;
|
||||
}
|
||||
|
||||
interface UsageEmbeddedWallets {
|
||||
countWalletAddresses: number;
|
||||
}
|
||||
|
||||
export interface UsageBillableByService {
|
||||
usage: {
|
||||
bundler: UsageBundler[];
|
||||
storage: UsageStorage;
|
||||
embeddedWallets: UsageEmbeddedWallets;
|
||||
};
|
||||
billableUsd: {
|
||||
bundler: number;
|
||||
storage: number;
|
||||
embeddedWallets: number;
|
||||
};
|
||||
limits: {
|
||||
storage: number;
|
||||
embeddedWallets: number;
|
||||
};
|
||||
rateLimits: {
|
||||
storage: number;
|
||||
@@ -231,36 +200,6 @@ export interface UsageBillableByService {
|
||||
};
|
||||
}
|
||||
|
||||
export interface WalletStats {
|
||||
date: string;
|
||||
uniqueWalletsConnected: number;
|
||||
totalConnections: number;
|
||||
walletType: string;
|
||||
}
|
||||
|
||||
export interface InAppWalletStats {
|
||||
date: string;
|
||||
authenticationMethod: string;
|
||||
uniqueWalletsConnected: number;
|
||||
}
|
||||
|
||||
export interface EcosystemWalletStats extends InAppWalletStats {}
|
||||
|
||||
export interface UserOpStats {
|
||||
date: string;
|
||||
successful: number;
|
||||
failed: number;
|
||||
sponsoredUsd: number;
|
||||
}
|
||||
|
||||
export interface UserOpStatsByChain {
|
||||
date: string;
|
||||
successful: number;
|
||||
failed: number;
|
||||
sponsoredUsd: number;
|
||||
chainId?: string;
|
||||
}
|
||||
|
||||
interface BillingProduct {
|
||||
name: string;
|
||||
id: string;
|
||||
@@ -317,30 +256,6 @@ export function useAccount({ refetchInterval }: UseAccountInput = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
export function useAccountUsage() {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery({
|
||||
queryKey: accountKeys.usage(user?.address as string),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/usage`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data as UsageBillableByService;
|
||||
},
|
||||
enabled: !!user?.address && isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAccountCredits() {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
@@ -373,43 +288,6 @@ export function useAccountCredits() {
|
||||
});
|
||||
}
|
||||
|
||||
async function getWalletUsage(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
}) {
|
||||
const { clientId, from, to, period } = args;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append("clientId", clientId);
|
||||
if (from) {
|
||||
searchParams.append("from", from.toISOString());
|
||||
}
|
||||
if (to) {
|
||||
searchParams.append("to", to.toISOString());
|
||||
}
|
||||
if (period) {
|
||||
searchParams.append("period", period);
|
||||
}
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_ANALYTICS_API_HOST}/v1/wallets?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
const json = await res.json();
|
||||
|
||||
if (res.status !== 200) {
|
||||
throw new Error(json.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
async function getUserOpUsage(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
@@ -447,43 +325,6 @@ async function getUserOpUsage(args: {
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export async function getInAppWalletUsage(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period?: "day" | "week" | "month" | "year" | "all";
|
||||
}) {
|
||||
const { clientId, from, to, period } = args;
|
||||
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.append("clientId", clientId);
|
||||
if (from) {
|
||||
searchParams.append("from", from.toISOString());
|
||||
}
|
||||
if (to) {
|
||||
searchParams.append("to", to.toISOString());
|
||||
}
|
||||
if (period) {
|
||||
searchParams.append("period", period);
|
||||
}
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_ANALYTICS_API_HOST}/v1/wallets/in-app?${searchParams.toString()}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
const json = await res?.json();
|
||||
|
||||
if (!res || res.status !== 200) {
|
||||
throw new Error(json.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
}
|
||||
|
||||
export function useUserOpUsageAggregate(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
@@ -567,63 +408,6 @@ export function useUserOpUsagePeriod(args: {
|
||||
});
|
||||
}
|
||||
|
||||
export function useWalletUsageAggregate(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
}) {
|
||||
const { clientId, from, to } = args;
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery({
|
||||
queryKey: accountKeys.walletStats(
|
||||
user?.address as string,
|
||||
clientId as string,
|
||||
from?.toISOString() || "",
|
||||
to?.toISOString() || "",
|
||||
"all",
|
||||
),
|
||||
queryFn: async () => {
|
||||
return getWalletUsage({
|
||||
clientId,
|
||||
from,
|
||||
to,
|
||||
period: "all",
|
||||
});
|
||||
},
|
||||
enabled: !!clientId && !!user?.address && isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useWalletUsagePeriod(args: {
|
||||
clientId: string;
|
||||
from?: Date;
|
||||
to?: Date;
|
||||
period: "day" | "week" | "month" | "year";
|
||||
}) {
|
||||
const { clientId, from, to, period } = args;
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery({
|
||||
queryKey: accountKeys.walletStats(
|
||||
user?.address as string,
|
||||
clientId as string,
|
||||
from?.toISOString() || "",
|
||||
to?.toISOString() || "",
|
||||
period,
|
||||
),
|
||||
queryFn: async () => {
|
||||
return getWalletUsage({
|
||||
clientId,
|
||||
from,
|
||||
to,
|
||||
period,
|
||||
});
|
||||
},
|
||||
enabled: !!clientId && !!user?.address && isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateAccount() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -658,51 +442,6 @@ export function useUpdateAccount() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateAccountPlan(waitForWebhook?: boolean) {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: { plan: string; feedback?: string }) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/plan`, {
|
||||
method: "PUT",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(input),
|
||||
});
|
||||
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
// Wait for account plan to update via stripe webhook
|
||||
// TODO: find a better way to notify the client that the plan has been updated
|
||||
if (waitForWebhook) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000 * 10));
|
||||
}
|
||||
|
||||
return json.data;
|
||||
},
|
||||
onSuccess: async () => {
|
||||
return Promise.all([
|
||||
// invalidate usage data as limits are different
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.usage(user?.address as string),
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateNotifications() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -735,36 +474,6 @@ export function useUpdateNotifications() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateBillingSession(enabled = false) {
|
||||
const { user } = useLoggedInUser();
|
||||
|
||||
return useQuery({
|
||||
queryKey: accountKeys.billingSession(user?.address as string),
|
||||
queryFn: async () => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/account/billingSession`,
|
||||
{
|
||||
method: "GET",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
},
|
||||
);
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
},
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
|
||||
export function useConfirmEmail() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -787,7 +496,7 @@ export function useConfirmEmail() {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
return json.data as { team: Team; account: Account };
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// invalidate related cache, since could be relinking account
|
||||
@@ -841,40 +550,6 @@ export function useResendEmailConfirmation() {
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreatePaymentMethod() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (paymentMethodId: string) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/paymentMethod`, {
|
||||
method: "POST",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
paymentMethodId,
|
||||
}),
|
||||
});
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useApiKeys() {
|
||||
const { user, isLoggedIn } = useLoggedInUser();
|
||||
return useQuery({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { ResultItem } from "app/team/[team_slug]/[project_slug]/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { ResultItem } from "app/team/[team_slug]/(team)/~/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
import type { EngineBackendWalletType } from "lib/engine";
|
||||
import { useState } from "react";
|
||||
|
||||
+14
-21
@@ -1,6 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
|
||||
@@ -15,16 +13,17 @@ import { useLoggedInUser } from "@3rdweb-sdk/react/hooks/useLoggedInUser";
|
||||
import { Turnstile } from "@marsidev/react-turnstile";
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { CanClaimResponseType } from "app/api/testnet-faucet/can-claim/CanClaimResponseType";
|
||||
import { Onboarding } from "components/onboarding";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { toUnits } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount, useWalletBalance } from "thirdweb/react";
|
||||
import { z } from "zod";
|
||||
import { isOnboardingComplete } from "../../../../../../login/isOnboardingRequired";
|
||||
|
||||
function formatTime(seconds: number) {
|
||||
const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });
|
||||
@@ -54,6 +53,7 @@ export function FaucetButton({
|
||||
chain: ChainMetadata;
|
||||
amount: number;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const client = useThirdwebClient();
|
||||
const address = useActiveAccount()?.address;
|
||||
const chainId = chain.chainId;
|
||||
@@ -120,7 +120,6 @@ export function FaucetButton({
|
||||
|
||||
const accountQuery = useAccount();
|
||||
const userQuery = useLoggedInUser();
|
||||
const [showOnboarding, setShowOnBoarding] = useState(false);
|
||||
|
||||
const canClaimFaucetQuery = useQuery({
|
||||
queryKey: ["testnet-faucet-can-claim", chainId],
|
||||
@@ -147,7 +146,8 @@ export function FaucetButton({
|
||||
return (
|
||||
<CustomConnectWallet
|
||||
loginRequired={true}
|
||||
connectButtonClassName="!w-full !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm"
|
||||
loadingButtonClassName="!w-full"
|
||||
signInLinkButtonClassName="!w-full !h-auto !rounded !bg-primary !text-primary-foreground !px-4 !py-2 !text-sm hover:!bg-primary/80"
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -203,24 +203,17 @@ export function FaucetButton({
|
||||
);
|
||||
}
|
||||
|
||||
// Email verification is required to claim from the faucet
|
||||
if (accountQuery.data.status === "noCustomer") {
|
||||
if (!isOnboardingComplete(accountQuery.data)) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="!opacity-100 w-full"
|
||||
onClick={() => setShowOnBoarding(true)}
|
||||
<Button asChild className="w-full">
|
||||
<Link
|
||||
href={
|
||||
pathname ? `/login?next=${encodeURIComponent(pathname)}` : "/login"
|
||||
}
|
||||
>
|
||||
Verify your Email
|
||||
</Button>
|
||||
{/* We will show the modal only if the user click on it, because this is a public page */}
|
||||
{showOnboarding && (
|
||||
<ChakraProviderSetup>
|
||||
<Onboarding onOpenChange={setShowOnBoarding} />
|
||||
</ChakraProviderSetup>
|
||||
)}
|
||||
</>
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ export function ChainHeader(props: ChainHeaderProps) {
|
||||
}
|
||||
/>
|
||||
<Button variant="default" asChild>
|
||||
<Link href="https://thirdweb.com/dashboard" target="_blank">
|
||||
<Link href="/team" target="_blank">
|
||||
Get started with thirdweb
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
@@ -205,7 +205,7 @@ export default async function ChainPageLayout(props: {
|
||||
}
|
||||
/>
|
||||
<Button variant="primary">
|
||||
<Link href="https://thirdweb.com/dashboard" target="_blank">
|
||||
<Link href="/team" target="_blank">
|
||||
Get started
|
||||
</Link>
|
||||
</Button>
|
||||
|
||||
+16
-34
@@ -22,6 +22,7 @@ import { isMoralisSupported } from "lib/wallet/nfts/moralis";
|
||||
import { isSimpleHashSupported } from "lib/wallet/nfts/simpleHash";
|
||||
import type { WalletNFT } from "lib/wallet/nfts/types";
|
||||
import { CircleAlertIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { type Dispatch, type SetStateAction, useMemo, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -47,14 +48,7 @@ import type {
|
||||
CreateListingParams,
|
||||
} from "thirdweb/extensions/marketplace";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import {
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Heading,
|
||||
Link,
|
||||
Text,
|
||||
} from "tw-components";
|
||||
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
|
||||
@@ -357,9 +351,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
})}
|
||||
>
|
||||
<FormControl>
|
||||
<Heading as={FormLabel} size="label.lg">
|
||||
Select NFT
|
||||
</Heading>
|
||||
<FormLabel>Select NFT</FormLabel>
|
||||
<FormHelperText mb="8px">
|
||||
Select the NFT you want to list for sale
|
||||
</FormHelperText>
|
||||
@@ -367,24 +359,22 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
<Flex flexDir="column" gap={4} mb={4}>
|
||||
<div className="flex flex-row items-center gap-3 rounded-md border border-border border-orange-100 bg-orange-50 p-[10px] dark:border-orange-300 dark:bg-orange-300">
|
||||
<InfoIcon className="size-6 text-orange-400 dark:text-orange-900" />
|
||||
<Text color="orange.800" _dark={{ color: "orange.900" }}>
|
||||
<p className="text-orange-800 dark:text-orange-900">
|
||||
This chain is not supported by our NFT API yet, please enter the
|
||||
contract address of the NFT you want to list.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
<FormControl
|
||||
isInvalid={!!form.formState.errors.selected?.contractAddress}
|
||||
>
|
||||
<Heading as={FormLabel} size="label.lg">
|
||||
Contract address
|
||||
</Heading>
|
||||
<FormLabel>Contract address</FormLabel>
|
||||
<SolidityInput
|
||||
solidityType="address"
|
||||
formContext={form}
|
||||
{...form.register("selected.contractAddress", {
|
||||
required: "Contract address is required",
|
||||
})}
|
||||
placeholder=""
|
||||
placeholder="0x..."
|
||||
/>
|
||||
<FormErrorMessage>
|
||||
{form.formState.errors.selected?.contractAddress?.message}
|
||||
@@ -458,21 +448,19 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
) : nfts && nfts.length === 0 ? (
|
||||
<div className="flex flex-row items-center gap-3 rounded-md border border-border border-orange-100 bg-orange-50 p-[10px] dark:border-orange-300 dark:bg-orange-300">
|
||||
<InfoIcon className="size-6 text-orange-400 dark:text-orange-900" />
|
||||
<Text color="orange.800" _dark={{ color: "orange.900" }}>
|
||||
<p className="text-orange-800 dark:text-orange-900">
|
||||
There are no NFTs owned by this wallet. You need NFTs to create a
|
||||
listing. You can create NFTs with thirdweb.{" "}
|
||||
<Link href="/explore/nft" color="blue.600" isExternal>
|
||||
<Link href="/explore/nft" color="blue.600" target="_blank">
|
||||
Explore NFT contracts
|
||||
</Link>
|
||||
.
|
||||
</Text>
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
</FormControl>
|
||||
<FormControl isRequired isDisabled={noNfts}>
|
||||
<Heading as={FormLabel} size="label.lg">
|
||||
Listing Currency
|
||||
</Heading>
|
||||
<FormLabel>Listing Currency</FormLabel>
|
||||
<CurrencySelector
|
||||
contractChainId={chainId}
|
||||
value={form.watch("currencyContractAddress")}
|
||||
@@ -485,11 +473,11 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl isRequired isDisabled={noNfts}>
|
||||
<Heading as={FormLabel} size="label.lg">
|
||||
<FormLabel>
|
||||
{form.watch("listingType") === "auction"
|
||||
? "Buyout Price Per Token"
|
||||
: "Listing Price"}
|
||||
</Heading>
|
||||
</FormLabel>
|
||||
<Input {...form.register("pricePerToken")} />
|
||||
<FormHelperText>
|
||||
{form.watch("listingType") === "auction"
|
||||
@@ -500,9 +488,7 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
{form.watch("selected")?.type?.toLowerCase() !== "erc721" && (
|
||||
<FormControl isRequired isDisabled={noNfts}>
|
||||
<div className="flex flex-row justify-between gap-2">
|
||||
<Heading as={FormLabel} size="label.lg">
|
||||
Quantity
|
||||
</Heading>
|
||||
<FormLabel>Quantity</FormLabel>
|
||||
</div>
|
||||
<Input {...form.register("quantity")} />
|
||||
<FormHelperText>
|
||||
@@ -513,18 +499,14 @@ export const CreateListingsForm: React.FC<CreateListingsFormProps> = ({
|
||||
{form.watch("listingType") === "auction" && (
|
||||
<>
|
||||
<FormControl isRequired isDisabled={noNfts}>
|
||||
<Heading as={FormLabel} size="label.lg">
|
||||
Reserve Price Per Token
|
||||
</Heading>
|
||||
<FormLabel>Reserve Price Per Token</FormLabel>
|
||||
<Input {...form.register("reservePricePerToken")} />
|
||||
<FormHelperText>
|
||||
The minimum price per token necessary to bid on this auction
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<FormControl isRequired>
|
||||
<Heading as={FormLabel} size="label.lg">
|
||||
Auction Duration
|
||||
</Heading>
|
||||
<FormLabel>Auction Duration</FormLabel>
|
||||
<Select {...form.register("listingDurationInSeconds")}>
|
||||
{auctionTimes.map((time) => (
|
||||
<option key={time.value} value={time.value}>
|
||||
|
||||
+31
-41
@@ -1,6 +1,8 @@
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { CopyAddressButton } from "@/components/ui/CopyAddressButton";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { Sheet, SheetContent } from "@/components/ui/sheet";
|
||||
import { Flex, GridItem, SimpleGrid, usePrevious } from "@chakra-ui/react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
@@ -9,7 +11,8 @@ import type {
|
||||
EnglishAuction,
|
||||
} from "thirdweb/extensions/marketplace";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Badge, Card, CodeBlock, Heading, Text } from "tw-components";
|
||||
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { CancelTab } from "./cancel-tab";
|
||||
import { LISTING_STATUS } from "./types";
|
||||
@@ -51,18 +54,20 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
height="150px"
|
||||
/>
|
||||
<Flex flexDir="column" gap={2} w="70%">
|
||||
<Heading size="title.lg">{renderData.asset.metadata.name}</Heading>
|
||||
<Text size="label.md" noOfLines={6}>
|
||||
<p className="font-bold text-lg">
|
||||
{renderData.asset.metadata.name}
|
||||
</Text>
|
||||
</p>
|
||||
<p className="line-clamp-6 truncate text-base leading-normal">
|
||||
{renderData.asset.metadata.name}
|
||||
</p>
|
||||
</Flex>
|
||||
</div>
|
||||
|
||||
<Flex flexDir="column" gap={4}>
|
||||
<Card as={Flex} flexDir="column" gap={3}>
|
||||
<Card className="flex flex-col gap-3 p-4">
|
||||
<SimpleGrid rowGap={3} columns={12} placeItems="center left">
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Asset contract address</Heading>
|
||||
<p className="font-bold">Asset contract address</p>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<CopyAddressButton
|
||||
@@ -71,7 +76,7 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Token ID</Heading>
|
||||
<p className="font-bold">Token ID</p>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<CopyTextButton
|
||||
@@ -82,13 +87,13 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Seller</Heading>
|
||||
<p className="font-bold">Seller</p>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<WalletAddress address={renderData.creatorAddress} />
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Listing ID</Heading>
|
||||
<p className="font-bold">Listing ID</p>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<CopyTextButton
|
||||
@@ -99,49 +104,37 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
/>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Type</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<Text fontFamily="mono" size="body.md">
|
||||
{renderData.asset.type}
|
||||
</Text>
|
||||
<p className="font-bold">Type</p>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>{renderData.asset.type}</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Status</Heading>
|
||||
<p className="font-bold">Status</p>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<Badge
|
||||
size="label.sm"
|
||||
variant="subtle"
|
||||
textTransform="capitalize"
|
||||
>
|
||||
<Badge className="uppercase">
|
||||
{LISTING_STATUS[renderData.status]}
|
||||
</Badge>
|
||||
</GridItem>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Quantity</Heading>
|
||||
<p className="font-bold">Quantity</p>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<Text fontFamily="mono" size="body.md">
|
||||
{(renderData.quantity || 0n).toString()}{" "}
|
||||
{/* For listings that are completed, the `quantity` would be `0`
|
||||
{(renderData.quantity || 0n).toString()}{" "}
|
||||
{/* For listings that are completed, the `quantity` would be `0`
|
||||
So we show this text to make it clear */}
|
||||
{LISTING_STATUS[renderData.status] === "Completed"
|
||||
? "(Sold out)"
|
||||
: ""}
|
||||
</Text>
|
||||
{LISTING_STATUS[renderData.status] === "Completed"
|
||||
? "(Sold out)"
|
||||
: ""}
|
||||
</GridItem>
|
||||
|
||||
{renderData.type === "direct-listing" && (
|
||||
<>
|
||||
<GridItem colSpan={3}>
|
||||
<Heading size="label.md">Price</Heading>
|
||||
<p className="font-bold">Price</p>
|
||||
</GridItem>
|
||||
<GridItem colSpan={9}>
|
||||
<Text fontFamily="mono" size="body.md">
|
||||
{renderData.currencyValuePerToken.displayValue}{" "}
|
||||
{renderData.currencyValuePerToken.symbol}
|
||||
</Text>
|
||||
{renderData.currencyValuePerToken.displayValue}{" "}
|
||||
{renderData.currencyValuePerToken.symbol}
|
||||
</GridItem>
|
||||
</>
|
||||
)}
|
||||
@@ -153,16 +146,13 @@ export const ListingDrawer: React.FC<NFTDrawerProps> = ({
|
||||
</SimpleGrid>
|
||||
</Card>
|
||||
{data?.asset.metadata.properties ? (
|
||||
<Card as={Flex} flexDir="column" gap={4}>
|
||||
<Heading size="label.md">Attributes</Heading>
|
||||
<CodeBlock
|
||||
<Card className="flex flex-col gap-3 p-4">
|
||||
<p className="font-bold">Attributes</p>
|
||||
<CodeClient
|
||||
code={
|
||||
JSON.stringify(data.asset.metadata.properties, null, 2) || ""
|
||||
}
|
||||
language="json"
|
||||
canCopy={false}
|
||||
wrap={false}
|
||||
overflow="auto"
|
||||
lang="json"
|
||||
/>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
+17
-17
@@ -1,7 +1,6 @@
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
ButtonGroup,
|
||||
Flex,
|
||||
IconButton,
|
||||
Select,
|
||||
Skeleton,
|
||||
@@ -38,7 +37,6 @@ import type {
|
||||
EnglishAuction,
|
||||
} from "thirdweb/extensions/marketplace";
|
||||
import { min } from "thirdweb/utils";
|
||||
import { Button, Text } from "tw-components";
|
||||
import { ListingDrawer } from "./listing-drawer";
|
||||
import { LISTING_STATUS } from "./types";
|
||||
|
||||
@@ -73,9 +71,9 @@ const tableColumns: Column<DirectListing | EnglishAuction>[] = [
|
||||
// biome-ignore lint/suspicious/noExplicitAny: FIXME
|
||||
Cell: ({ cell }: { cell: Cell<any, any> }) => {
|
||||
return (
|
||||
<Text size="label.md" whiteSpace="nowrap">
|
||||
<p className="whitespace-nowrap text-muted-foreground">
|
||||
{cell.value.displayValue} {cell.value.symbol}
|
||||
</Text>
|
||||
</p>
|
||||
);
|
||||
},
|
||||
},
|
||||
@@ -182,21 +180,23 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
>(null);
|
||||
|
||||
return (
|
||||
<Flex gap={4} direction="column">
|
||||
<ButtonGroup size="sm" variant="outline" isAttached>
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-row">
|
||||
<Button
|
||||
onClick={() => setListingsToShow("all")}
|
||||
variant={listingsToShow === "all" ? "solid" : "outline"}
|
||||
variant={listingsToShow === "all" ? "default" : "outline"}
|
||||
className="w-18 rounded-r-none"
|
||||
>
|
||||
All
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setListingsToShow("valid")}
|
||||
variant={listingsToShow === "valid" ? "solid" : "outline"}
|
||||
variant={listingsToShow === "valid" ? "default" : "outline"}
|
||||
className="rounded-l-none"
|
||||
>
|
||||
Valid
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
<TableContainer maxW="100%" className="relative">
|
||||
{((listingsToShow === "all" && getAllQueryResult.isFetching) ||
|
||||
(listingsToShow === "valid" && getValidQueryResult.isFetching)) && (
|
||||
@@ -226,9 +226,9 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
key={columnIndex}
|
||||
>
|
||||
<Text as="label" size="label.sm" color="faded">
|
||||
<p className="text-muted-foreground">
|
||||
{column.render("Header")}
|
||||
</Text>
|
||||
</p>
|
||||
</Th>
|
||||
))}
|
||||
{/* // Need to add an empty header for the drawer button */}
|
||||
@@ -273,7 +273,7 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<div className="flex w-full items-center justify-center">
|
||||
<Flex gap={2} direction="row" align="center">
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<IconButton
|
||||
isDisabled={!canPreviousPage || totalCountQuery.isPending}
|
||||
aria-label="first page"
|
||||
@@ -286,7 +286,7 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
icon={<ChevronLeftIcon className="size-4" />}
|
||||
onClick={() => previousPage()}
|
||||
/>
|
||||
<Text whiteSpace="nowrap">
|
||||
<p className="whitespace-nowrap">
|
||||
Page <strong>{pageIndex + 1}</strong> of{" "}
|
||||
<Skeleton
|
||||
as="span"
|
||||
@@ -295,7 +295,7 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
>
|
||||
<strong>{pageCount}</strong>
|
||||
</Skeleton>
|
||||
</Text>
|
||||
</p>
|
||||
<IconButton
|
||||
isDisabled={!canNextPage || totalCountQuery.isPending}
|
||||
aria-label="next page"
|
||||
@@ -322,8 +322,8 @@ export const MarketplaceTable: React.FC<MarketplaceTableProps> = ({
|
||||
<option value="250">250</option>
|
||||
<option value="500">500</option>
|
||||
</Select>
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
</Flex>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Heading } from "tw-components";
|
||||
import { CreateListingButton } from "../components/list-button";
|
||||
import { DirectListingsTable } from "./components/table";
|
||||
|
||||
@@ -15,7 +14,7 @@ export const ContractDirectListingsPage: React.FC<
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<Heading size="title.sm">Contract Listings</Heading>
|
||||
<p className="text-lg">Contract Listings</p>
|
||||
<div className="flex flex-row gap-4">
|
||||
<CreateListingButton
|
||||
contract={contract}
|
||||
|
||||
+1
-2
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Heading } from "tw-components";
|
||||
import { CreateListingButton } from "../components/list-button";
|
||||
import { EnglishAuctionsTable } from "./components/table";
|
||||
|
||||
@@ -15,7 +14,7 @@ export const ContractEnglishAuctionsPage: React.FC<
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<Heading size="title.sm">Contract Auctions</Heading>
|
||||
<p className="text-lg">Contract Auctions</p>
|
||||
<div className="flex flex-row gap-4">
|
||||
<CreateListingButton
|
||||
contract={contract}
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { Link } from "tw-components";
|
||||
import Link from "next/link";
|
||||
import { QuantityInputWithUnlimited } from "../../quantity-input-with-unlimited";
|
||||
import { CustomFormControl } from "../common";
|
||||
import { useClaimConditionsFormContext } from "../index";
|
||||
@@ -41,8 +41,8 @@ export const MaxClaimablePerWalletInput: React.FC = () => {
|
||||
Limits are set per wallets and not per user, sophisticated actors
|
||||
could get around wallet restrictions.{" "}
|
||||
<Link
|
||||
isExternal
|
||||
color="blue.500"
|
||||
className="text-blue-500"
|
||||
target="_blank"
|
||||
href="https://portal.thirdweb.com/contracts/design/Drop#sybil-attacks"
|
||||
>
|
||||
Learn more
|
||||
|
||||
+2
-9
@@ -1,11 +1,6 @@
|
||||
import { Flex, FormControl } from "@chakra-ui/react";
|
||||
import type { FieldError } from "react-hook-form";
|
||||
import {
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Heading,
|
||||
} from "tw-components";
|
||||
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
|
||||
import type { ComponentWithChildren } from "types/component-with-children";
|
||||
import { useClaimConditionsFormContext } from ".";
|
||||
|
||||
@@ -22,9 +17,7 @@ export const CustomFormControl: ComponentWithChildren<
|
||||
return (
|
||||
<FormControl isDisabled={props.disabled} isInvalid={!!props.error}>
|
||||
{/* label */}
|
||||
<Heading as={FormLabel} size="label.md">
|
||||
{props.label}
|
||||
</Heading>
|
||||
<FormLabel className="font-bold">{props.label}</FormLabel>
|
||||
|
||||
{/* input */}
|
||||
{props.children}
|
||||
|
||||
+1
-1
@@ -454,7 +454,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
};
|
||||
}, [claimConditionsQuery.data, controlledFields, isMultiPhase]);
|
||||
|
||||
if (isFetchingData) {
|
||||
if (claimConditionsQuery.isPending) {
|
||||
return (
|
||||
<div className="flex h-[400px] w-full items-center justify-center rounded-lg border border-border">
|
||||
<Spinner className="size-10" />
|
||||
|
||||
+98
-100
@@ -1,8 +1,9 @@
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { AdminOnly } from "@3rdweb-sdk/react/components/roles/admin-only";
|
||||
import { Flex, SimpleGrid } from "@chakra-ui/react";
|
||||
import { ChevronDownIcon, ChevronUpIcon, XIcon } from "lucide-react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Badge, Button, Card, Heading, Text } from "tw-components";
|
||||
import { ClaimConditionTypeData, useClaimConditionsFormContext } from ".";
|
||||
import { PricePreview } from "../price-preview";
|
||||
import { ClaimPriceInput } from "./Inputs/ClaimPriceInput";
|
||||
@@ -41,117 +42,114 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<Card position="relative" p={8}>
|
||||
<Flex direction="column" gap={8}>
|
||||
<Flex
|
||||
align="flex-start"
|
||||
justify="space-between"
|
||||
position="absolute"
|
||||
top="10px"
|
||||
right="10px"
|
||||
gap={1}
|
||||
<Card className="relative flex flex-col gap-8 p-8">
|
||||
<div className="absolute top-3 right-3 flex flex-row items-start justify-between gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={toggleEditing}
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
>
|
||||
{field.isEditing ? "Collapse" : isAdmin ? "Edit" : "See Phase"}
|
||||
{field.isEditing ? (
|
||||
<ChevronUpIcon className="size-4" />
|
||||
) : (
|
||||
<ChevronDownIcon className="size-4" />
|
||||
)}
|
||||
</Button>
|
||||
<AdminOnly contract={contract}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={toggleEditing}
|
||||
onClick={onRemove}
|
||||
disabled={isPending}
|
||||
size="sm"
|
||||
rightIcon={
|
||||
field.isEditing ? (
|
||||
<ChevronUpIcon className="size-4" />
|
||||
) : (
|
||||
<ChevronDownIcon className="size-4" />
|
||||
)
|
||||
}
|
||||
className="gap-2 text-red-300"
|
||||
>
|
||||
{field.isEditing ? "Collapse" : isAdmin ? "Edit" : "See Phase"}
|
||||
Remove <XIcon className="size-4" />
|
||||
</Button>
|
||||
<AdminOnly contract={contract}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={onRemove}
|
||||
isDisabled={isPending}
|
||||
colorScheme="red"
|
||||
size="sm"
|
||||
rightIcon={<XIcon className="size-4" />}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</AdminOnly>
|
||||
</Flex>
|
||||
</AdminOnly>
|
||||
</div>
|
||||
|
||||
<Flex flexDir="column" gap={2} mt={{ base: 4, md: 0 }}>
|
||||
<Flex gap={3} alignItems="center">
|
||||
<Heading>{ClaimConditionTypeData[claimConditionType].name}</Heading>
|
||||
{isActive && (
|
||||
<Badge colorScheme="green" borderRadius="lg" p={1.5}>
|
||||
Currently active
|
||||
</Badge>
|
||||
<div className="mt-4 flex flex-col gap-2 md:mt-0">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<p className="font-bold text-lg text-muted-foreground">
|
||||
{ClaimConditionTypeData[claimConditionType].name}
|
||||
</p>
|
||||
{isActive && (
|
||||
<Badge variant="success" className="rounded-lg p-1.5">
|
||||
Currently active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground">
|
||||
{ClaimConditionTypeData[claimConditionType].description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{!field.isEditing ? (
|
||||
<div className="grid grid-cols-2 gap-2 md:grid-cols-4">
|
||||
<div className="flex flex-col">
|
||||
<p className="font-bold text-muted-foreground">Phase start</p>
|
||||
<p className="text-muted-foreground">
|
||||
{field.startTime?.toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<p className="font-bold text-muted-foreground">
|
||||
{isErc20 ? "Tokens" : "NFTs"} to drop
|
||||
</p>
|
||||
<p className="text-muted-foreground capitalize">
|
||||
{field.maxClaimableSupply}
|
||||
</p>
|
||||
</div>
|
||||
<PricePreview
|
||||
price={field.price}
|
||||
currencyAddress={field.currencyAddress}
|
||||
contractChainId={contract.chain.id}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p className="font-bold text-muted-foreground">Limit per wallet</p>
|
||||
{claimConditionType === "specific" ? (
|
||||
<p>Set in the snapshot</p>
|
||||
) : claimConditionType === "creator" ? (
|
||||
<p>Unlimited</p>
|
||||
) : (
|
||||
<p className="text-muted-foreground capitalize">
|
||||
{field.maxClaimablePerWallet}
|
||||
</p>
|
||||
)}
|
||||
</Flex>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<CustomFormGroup>
|
||||
{/* Phase Name Input / Form Title */}
|
||||
{isMultiPhase ? <PhaseNameInput /> : null}
|
||||
<PhaseStartTimeInput />
|
||||
</CustomFormGroup>
|
||||
|
||||
<Text>{ClaimConditionTypeData[claimConditionType].description}</Text>
|
||||
</Flex>
|
||||
<CreatorInput
|
||||
creatorAddress={
|
||||
(field.snapshot?.[0] as { address: string })?.address
|
||||
}
|
||||
/>
|
||||
|
||||
{!field.isEditing ? (
|
||||
<SimpleGrid columns={{ base: 2, md: 4 }} gap={2}>
|
||||
<div className="flex flex-col">
|
||||
<Text fontWeight="bold">Phase start</Text>
|
||||
<Text>{field.startTime?.toLocaleString()}</Text>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<Text fontWeight="bold">
|
||||
{isErc20 ? "Tokens" : "NFTs"} to drop
|
||||
</Text>
|
||||
<Text textTransform="capitalize">{field.maxClaimableSupply}</Text>
|
||||
</div>
|
||||
<PricePreview
|
||||
price={field.price}
|
||||
currencyAddress={field.currencyAddress}
|
||||
contractChainId={contract.chain.id}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<Text fontWeight="bold">Limit per wallet</Text>
|
||||
{claimConditionType === "specific" ? (
|
||||
<Text>Set in the snapshot</Text>
|
||||
) : claimConditionType === "creator" ? (
|
||||
<Text>Unlimited</Text>
|
||||
) : (
|
||||
<Text textTransform="capitalize">
|
||||
{field.maxClaimablePerWallet}
|
||||
</Text>
|
||||
)}
|
||||
</div>
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<>
|
||||
<CustomFormGroup>
|
||||
<MaxClaimableSupplyInput />
|
||||
<ClaimPriceInput contractChainId={contract.chain.id} />
|
||||
</CustomFormGroup>
|
||||
|
||||
{claimConditionType === "specific" ||
|
||||
claimConditionType === "creator" ? null : (
|
||||
<CustomFormGroup>
|
||||
{/* Phase Name Input / Form Title */}
|
||||
{isMultiPhase ? <PhaseNameInput /> : null}
|
||||
<PhaseStartTimeInput />
|
||||
<MaxClaimablePerWalletInput />
|
||||
</CustomFormGroup>
|
||||
)}
|
||||
|
||||
<CreatorInput
|
||||
creatorAddress={
|
||||
(field.snapshot?.[0] as { address: string })?.address
|
||||
}
|
||||
/>
|
||||
|
||||
<CustomFormGroup>
|
||||
<MaxClaimableSupplyInput />
|
||||
<ClaimPriceInput contractChainId={contract.chain.id} />
|
||||
</CustomFormGroup>
|
||||
|
||||
{claimConditionType === "specific" ||
|
||||
claimConditionType === "creator" ? null : (
|
||||
<CustomFormGroup>
|
||||
<MaxClaimablePerWalletInput />
|
||||
</CustomFormGroup>
|
||||
)}
|
||||
|
||||
<ClaimerSelection />
|
||||
</>
|
||||
)}
|
||||
</Flex>
|
||||
<ClaimerSelection />
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-6
@@ -1,6 +1,5 @@
|
||||
import { CURRENCIES } from "constants/currencies";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { Text } from "tw-components";
|
||||
import { shortenIfAddress } from "utils/usedapp-external";
|
||||
import { isAddressZero } from "utils/zeroAddress";
|
||||
|
||||
@@ -25,19 +24,19 @@ export const PricePreview: React.FC<PricePreviewProps> = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Text fontWeight="bold">Default price</Text>
|
||||
<div className="flex flex-col text-muted-foreground">
|
||||
<p className="font-bold">Default price</p>
|
||||
{Number(price) === 0 ? (
|
||||
<Text>Free</Text>
|
||||
<p>Free</p>
|
||||
) : (
|
||||
<Text>
|
||||
<p>
|
||||
{price}{" "}
|
||||
{foundCurrency
|
||||
? foundCurrency.symbol
|
||||
: isAddressZero(currencyAddress || "")
|
||||
? chain?.nativeCurrency.symbol || "(Native Token)"
|
||||
: `(${shortenIfAddress(currencyAddress)})`}
|
||||
</Text>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
+21
-28
@@ -1,18 +1,14 @@
|
||||
import {
|
||||
Input,
|
||||
InputGroup,
|
||||
type InputProps,
|
||||
InputRightElement,
|
||||
} from "@chakra-ui/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "tw-components";
|
||||
|
||||
interface QuantityInputWithUnlimitedProps
|
||||
extends Omit<InputProps, "onChange" | "value" | "onBlur" | "max" | "min"> {
|
||||
interface QuantityInputWithUnlimitedProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
hideMaxButton?: true;
|
||||
decimals?: number;
|
||||
isDisabled: boolean;
|
||||
isRequired: boolean;
|
||||
}
|
||||
|
||||
export const QuantityInputWithUnlimited: React.FC<
|
||||
@@ -24,7 +20,6 @@ export const QuantityInputWithUnlimited: React.FC<
|
||||
isDisabled,
|
||||
isRequired,
|
||||
decimals,
|
||||
...restInputProps
|
||||
}) => {
|
||||
const [stringValue, setStringValue] = useState<string>(
|
||||
Number.isNaN(Number(value)) ? "0" : value.toString(),
|
||||
@@ -50,10 +45,10 @@ export const QuantityInputWithUnlimited: React.FC<
|
||||
};
|
||||
|
||||
return (
|
||||
<InputGroup {...restInputProps}>
|
||||
<div className="flex flex-row items-center rounded-md border border-border">
|
||||
<Input
|
||||
isRequired={isRequired}
|
||||
isDisabled={isDisabled}
|
||||
required={isRequired}
|
||||
disabled={isDisabled}
|
||||
value={stringValue === "unlimited" ? "Unlimited" : stringValue}
|
||||
onChange={(e) => updateValue(e.currentTarget.value)}
|
||||
onBlur={() => {
|
||||
@@ -65,23 +60,21 @@ export const QuantityInputWithUnlimited: React.FC<
|
||||
setStringValue("0");
|
||||
}
|
||||
}}
|
||||
className="border-none"
|
||||
/>
|
||||
{hideMaxButton ? null : (
|
||||
<InputRightElement w="auto">
|
||||
<Button
|
||||
isDisabled={isDisabled}
|
||||
colorScheme="primary"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
mr={1}
|
||||
onClick={() => {
|
||||
updateValue("unlimited");
|
||||
}}
|
||||
>
|
||||
Unlimited
|
||||
</Button>
|
||||
</InputRightElement>
|
||||
<Button
|
||||
disabled={isDisabled}
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="mr-1 text-primary"
|
||||
onClick={() => {
|
||||
updateValue("unlimited");
|
||||
}}
|
||||
>
|
||||
Unlimited
|
||||
</Button>
|
||||
)}
|
||||
</InputGroup>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-3
@@ -17,7 +17,6 @@ import {
|
||||
ChevronRightIcon,
|
||||
} from "lucide-react";
|
||||
import { type Column, usePagination, useTable } from "react-table";
|
||||
import { Text } from "tw-components";
|
||||
|
||||
interface CsvDataTableProps<T extends object> {
|
||||
data: T[];
|
||||
@@ -78,9 +77,9 @@ export function CsvDataTable<T extends object>({
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
|
||||
key={columnIndex}
|
||||
>
|
||||
<Text as="label" size="label.sm" color="faded">
|
||||
<p className="text-muted-foreground">
|
||||
{column.render("Header")}
|
||||
</Text>
|
||||
</p>
|
||||
</Th>
|
||||
))}
|
||||
</Tr>
|
||||
|
||||
+4
-13
@@ -1,9 +1,10 @@
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Flex, SimpleGrid, useBreakpointValue } from "@chakra-ui/react";
|
||||
import { formatDistance } from "date-fns/formatDistance";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { Badge, Card, Heading, Text } from "tw-components";
|
||||
import { Card, Heading, Text } from "tw-components";
|
||||
|
||||
export type AccountSignerType = {
|
||||
signer: string;
|
||||
@@ -45,19 +46,9 @@ export const AccountSigner: React.FC<AccountSignerProps> = ({
|
||||
<WalletAddress shortenAddress={isMobile} address={signer} />
|
||||
</Heading>
|
||||
<div className="flex flex-row gap-2">
|
||||
{isAdmin ? (
|
||||
<Badge borderRadius="lg" p={1.5}>
|
||||
Admin Key
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge borderRadius="lg" p={1.5}>
|
||||
Scoped key
|
||||
</Badge>
|
||||
)}
|
||||
{isAdmin ? <Badge>Admin Key</Badge> : <Badge>Scoped key</Badge>}
|
||||
{signer === address && (
|
||||
<Badge colorScheme="green" borderRadius="lg" p={1.5}>
|
||||
Currently connected
|
||||
</Badge>
|
||||
<Badge variant="success">Currently connected</Badge>
|
||||
)}
|
||||
</div>
|
||||
</Flex>
|
||||
|
||||
+5
-14
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { useApiKeys, useCreateApiKey } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { Flex, FormControl, Input, Select } from "@chakra-ui/react";
|
||||
import { LazyCreateAPIKeyDialog } from "components/settings/ApiKeys/Create/LazyCreateAPIKeyDialog";
|
||||
@@ -16,14 +17,7 @@ import type { StoredChain } from "stores/chainStores";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import {
|
||||
Card,
|
||||
CodeBlock,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Heading,
|
||||
Text,
|
||||
} from "tw-components";
|
||||
import { Card, FormHelperText, FormLabel, Heading, Text } from "tw-components";
|
||||
|
||||
interface EmbedSetupProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -304,7 +298,6 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
return (
|
||||
<Flex gap={8} direction="column">
|
||||
<LazyCreateAPIKeyDialog
|
||||
wording="api-key"
|
||||
prefill={{
|
||||
name: "Embed API Key",
|
||||
domains: "embed.ipfscdn.io",
|
||||
@@ -536,12 +529,10 @@ export const EmbedSetup: React.FC<EmbedSetupProps> = ({
|
||||
|
||||
<Card className="flex w-full flex-col gap-2 md:w-1/2">
|
||||
<Heading size="title.sm">Embed Code</Heading>
|
||||
<CodeBlock
|
||||
canCopy={false}
|
||||
whiteSpace="pre"
|
||||
overflowX="auto"
|
||||
<CodeClient
|
||||
code={embedCode}
|
||||
language="markup"
|
||||
lang="html"
|
||||
loadingClassName="min-h-[190px]"
|
||||
/>
|
||||
<Button
|
||||
className="w-auto gap-2"
|
||||
|
||||
+8
-14
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import {
|
||||
type InternalTransaction,
|
||||
@@ -32,14 +33,7 @@ import { Fragment, useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { stringify } from "thirdweb/utils";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CodeBlock,
|
||||
FormLabel,
|
||||
Heading,
|
||||
Text,
|
||||
} from "tw-components";
|
||||
import { Button, Card, FormLabel, Heading, Text } from "tw-components";
|
||||
|
||||
interface EventsFeedProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -351,12 +345,12 @@ const EventsFeedItem: React.FC<EventsFeedItemProps> = ({
|
||||
<Box gridColumn="span 3">
|
||||
<Text fontWeight="bold">{event.eventName}</Text>
|
||||
</Box>
|
||||
|
||||
<CodeBlock
|
||||
gridColumn="span 9"
|
||||
code={stringify(event.args)}
|
||||
language="json"
|
||||
/>
|
||||
<Box gridColumn="span 9">
|
||||
<CodeClient
|
||||
code={stringify(event.args, null, 2)}
|
||||
lang="json"
|
||||
/>
|
||||
</Box>
|
||||
</SimpleGrid>
|
||||
|
||||
{arr.length - 1 === idx ? null : <Divider />}
|
||||
|
||||
+128
-118
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { FormFieldSetup } from "@/components/blocks/FormFieldSetup";
|
||||
import { DatePickerWithRange } from "@/components/ui/DatePickerWithRange";
|
||||
import {
|
||||
@@ -7,7 +8,7 @@ import {
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Alert, AlertTitle } from "@/components/ui/alert";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Form,
|
||||
@@ -18,19 +19,26 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { addDays, fromUnixTime } from "date-fns";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { CircleAlertIcon, PlusIcon, Trash2Icon } from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import {
|
||||
type Dispatch,
|
||||
type SetStateAction,
|
||||
useCallback,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useFieldArray, useForm } from "react-hook-form";
|
||||
import {
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type PreparedTransaction,
|
||||
ZERO_ADDRESS,
|
||||
getContract,
|
||||
@@ -57,22 +65,15 @@ export type ClaimConditionValue = {
|
||||
auxData: string;
|
||||
};
|
||||
|
||||
type ClaimCondition = {
|
||||
availableSupply: bigint;
|
||||
allowlistMerkleRoot: `0x${string}`;
|
||||
pricePerUnit: bigint;
|
||||
currency: string;
|
||||
maxMintPerWallet: bigint;
|
||||
startTimestamp: number;
|
||||
endTimestamp: number;
|
||||
auxData: string;
|
||||
};
|
||||
const positiveIntegerRegex = /^[0-9]\d*$/;
|
||||
|
||||
function ClaimableModule(props: ModuleInstanceProps) {
|
||||
const { contract, ownerAccount } = props;
|
||||
const account = useActiveAccount();
|
||||
const [tokenId, setTokenId] = useState<string>("");
|
||||
|
||||
const isErc721 = props.contractInfo.name === "ClaimableERC721";
|
||||
const isValidTokenId = positiveIntegerRegex.test(tokenId);
|
||||
|
||||
const primarySaleRecipientQuery = useReadContract(
|
||||
isErc721 ? ClaimableERC721.getSaleConfig : ClaimableERC1155.getSaleConfig,
|
||||
@@ -82,20 +83,27 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
);
|
||||
|
||||
const claimConditionQuery = useReadContract(
|
||||
ClaimableERC721.getClaimCondition,
|
||||
isErc721
|
||||
? ClaimableERC721.getClaimCondition
|
||||
: ClaimableERC1155.getClaimCondition,
|
||||
{
|
||||
tokenId: positiveIntegerRegex.test(tokenId) ? BigInt(tokenId) : 0n,
|
||||
contract: contract,
|
||||
queryOptions: {
|
||||
enabled: isErc721,
|
||||
enabled: isErc721 || (!!tokenId && isValidTokenId),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const getClaimConditionErc1155 = (tokenId: string) =>
|
||||
ClaimableERC1155.getClaimCondition({
|
||||
contract: contract,
|
||||
tokenId: BigInt(tokenId),
|
||||
});
|
||||
const noClaimConditionSet =
|
||||
claimConditionQuery.data?.availableSupply === 0n &&
|
||||
claimConditionQuery.data?.allowlistMerkleRoot ===
|
||||
"0x0000000000000000000000000000000000000000000000000000000000000000" &&
|
||||
claimConditionQuery.data?.pricePerUnit === 0n &&
|
||||
claimConditionQuery.data?.currency === ZERO_ADDRESS &&
|
||||
claimConditionQuery.data?.maxMintPerWallet === 0n &&
|
||||
claimConditionQuery.data?.startTimestamp === 0 &&
|
||||
claimConditionQuery.data?.endTimestamp === 0;
|
||||
|
||||
const currencyContract = getContract({
|
||||
address: claimConditionQuery.data?.currency || "",
|
||||
@@ -104,7 +112,6 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
});
|
||||
|
||||
const shouldFetchTokenDecimals =
|
||||
isErc721 &&
|
||||
claimConditionQuery.data &&
|
||||
claimConditionQuery.data?.currency !== ZERO_ADDRESS;
|
||||
|
||||
@@ -217,10 +224,8 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
}}
|
||||
claimConditionSection={{
|
||||
data:
|
||||
// claim condition is common for all tokens
|
||||
isErc721 &&
|
||||
// claim conditions is fetched
|
||||
claimConditionQuery.isFetched &&
|
||||
// claim conditions data is present
|
||||
claimConditionQuery.data &&
|
||||
// token decimals is fetched if it should be fetched
|
||||
(shouldFetchTokenDecimals ? tokenDecimalsQuery.isFetched : true)
|
||||
? {
|
||||
@@ -229,11 +234,17 @@ function ClaimableModule(props: ModuleInstanceProps) {
|
||||
}
|
||||
: undefined,
|
||||
setClaimCondition,
|
||||
getClaimConditionErc1155,
|
||||
tokenId,
|
||||
isLoading:
|
||||
claimConditionQuery.isLoading ||
|
||||
(!!shouldFetchTokenDecimals && tokenDecimalsQuery.isLoading),
|
||||
}}
|
||||
isOwnerAccount={!!ownerAccount}
|
||||
isErc721={isErc721}
|
||||
contractChainId={props.contract.chain.id}
|
||||
setTokenId={setTokenId}
|
||||
isValidTokenId={isValidTokenId}
|
||||
noClaimConditionSet={noClaimConditionSet}
|
||||
mintSection={{
|
||||
mint,
|
||||
}}
|
||||
@@ -246,6 +257,9 @@ export function ClaimableModuleUI(
|
||||
isOwnerAccount: boolean;
|
||||
isErc721: boolean;
|
||||
contractChainId: number;
|
||||
setTokenId: Dispatch<SetStateAction<string>>;
|
||||
isValidTokenId: boolean;
|
||||
noClaimConditionSet: boolean;
|
||||
primarySaleRecipientSection: {
|
||||
setPrimarySaleRecipient: (
|
||||
values: PrimarySaleRecipientFormValues,
|
||||
@@ -260,14 +274,15 @@ export function ClaimableModuleUI(
|
||||
mint: (values: MintFormValues) => Promise<void>;
|
||||
};
|
||||
claimConditionSection: {
|
||||
tokenId: string;
|
||||
setClaimCondition: (values: ClaimConditionFormValues) => Promise<void>;
|
||||
getClaimConditionErc1155: (tokenId: string) => Promise<ClaimCondition>;
|
||||
data:
|
||||
| {
|
||||
claimCondition: ClaimConditionValue | undefined;
|
||||
claimCondition: ClaimConditionValue;
|
||||
tokenDecimals: number | undefined;
|
||||
}
|
||||
| undefined;
|
||||
isLoading: boolean;
|
||||
};
|
||||
},
|
||||
) {
|
||||
@@ -296,25 +311,44 @@ export function ClaimableModuleUI(
|
||||
Claim Conditions
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="px-1">
|
||||
{!props.isErc721 || props.claimConditionSection.data ? (
|
||||
<ClaimConditionSection
|
||||
isOwnerAccount={props.isOwnerAccount}
|
||||
claimCondition={
|
||||
props.claimConditionSection.data?.claimCondition
|
||||
}
|
||||
update={props.claimConditionSection.setClaimCondition}
|
||||
isErc721={props.isErc721}
|
||||
chainId={props.contractChainId}
|
||||
tokenDecimals={
|
||||
props.claimConditionSection.data?.tokenDecimals
|
||||
}
|
||||
getClaimConditionErc1155={
|
||||
props.claimConditionSection.getClaimConditionErc1155
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<Skeleton className="h-[350px]" />
|
||||
{!props.isErc721 && (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex-1 space-y-1">
|
||||
<Label>Token ID</Label>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{props.isOwnerAccount
|
||||
? "View and Update claim conditions for given token ID"
|
||||
: "View claim conditions for given token ID"}
|
||||
</p>
|
||||
<Input onChange={(e) => props.setTokenId(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
{props.isValidTokenId &&
|
||||
props.claimConditionSection.data &&
|
||||
!props.claimConditionSection.isLoading && (
|
||||
<ClaimConditionSection
|
||||
isOwnerAccount={props.isOwnerAccount}
|
||||
claimCondition={
|
||||
props.claimConditionSection.data.claimCondition
|
||||
}
|
||||
update={props.claimConditionSection.setClaimCondition}
|
||||
isErc721={props.isErc721}
|
||||
chainId={props.contractChainId}
|
||||
noClaimConditionSet={props.noClaimConditionSet}
|
||||
tokenDecimals={
|
||||
props.claimConditionSection.data?.tokenDecimals
|
||||
}
|
||||
tokenId={props.claimConditionSection.tokenId}
|
||||
/>
|
||||
)}
|
||||
{props.isValidTokenId &&
|
||||
props.claimConditionSection.isLoading && (
|
||||
<Skeleton className="h-[350px]" />
|
||||
)}
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
|
||||
@@ -383,63 +417,48 @@ const defaultStartDate = addDays(new Date(), 7);
|
||||
const defaultEndDate = addDays(new Date(), 14);
|
||||
|
||||
function ClaimConditionSection(props: {
|
||||
claimCondition?: ClaimConditionValue;
|
||||
claimCondition: ClaimConditionValue;
|
||||
update: (values: ClaimConditionFormValues) => Promise<void>;
|
||||
isOwnerAccount: boolean;
|
||||
isErc721: boolean;
|
||||
chainId: number;
|
||||
tokenDecimals?: number;
|
||||
getClaimConditionErc1155: (tokenId: string) => Promise<ClaimCondition>;
|
||||
tokenId: string;
|
||||
noClaimConditionSet: boolean;
|
||||
}) {
|
||||
const { idToChain } = useAllChainsData();
|
||||
const chain = idToChain.get(props.chainId);
|
||||
const { claimCondition } = props;
|
||||
|
||||
const tokenIdForm = useForm<{ tokenId: string }>({
|
||||
defaultValues: {
|
||||
tokenId: "",
|
||||
},
|
||||
});
|
||||
|
||||
const tokenId = tokenIdForm.watch("tokenId");
|
||||
|
||||
const claimConditionErc1155Query = useQuery({
|
||||
queryKey: ["claimConditionErc1155", props.chainId, tokenId],
|
||||
queryFn: () => props.getClaimConditionErc1155(tokenId),
|
||||
enabled: !props.isErc721 && BigInt(tokenId) >= 0n,
|
||||
});
|
||||
|
||||
const conditions = props.isErc721
|
||||
? claimCondition
|
||||
: claimConditionErc1155Query.data;
|
||||
const { tokenId, claimCondition } = props;
|
||||
const [addClaimConditionButtonClicked, setAddClaimConditionButtonClicked] =
|
||||
useState(false);
|
||||
|
||||
const form = useForm<ClaimConditionFormValues>({
|
||||
resolver: zodResolver(claimConditionFormSchema),
|
||||
values: {
|
||||
tokenId,
|
||||
currencyAddress:
|
||||
conditions?.currency === ZERO_ADDRESS ? "" : conditions?.currency,
|
||||
pricePerToken:
|
||||
conditions?.pricePerUnit &&
|
||||
conditions?.currency !== ZERO_ADDRESS &&
|
||||
props.tokenDecimals
|
||||
? Number(toTokens(conditions?.pricePerUnit, props.tokenDecimals))
|
||||
: 0,
|
||||
claimCondition?.currency === ZERO_ADDRESS
|
||||
? NATIVE_TOKEN_ADDRESS // default to the native token address
|
||||
: claimCondition?.currency,
|
||||
// default case is zero state, so 0 // 10 ** 18 still results in 0
|
||||
pricePerToken: Number(
|
||||
toTokens(claimCondition?.pricePerUnit, props.tokenDecimals || 18),
|
||||
),
|
||||
maxClaimableSupply:
|
||||
conditions?.availableSupply.toString() === "0" ||
|
||||
conditions?.availableSupply.toString() === MAX_UINT_256
|
||||
claimCondition?.availableSupply.toString() === "0" ||
|
||||
claimCondition?.availableSupply.toString() === MAX_UINT_256
|
||||
? ""
|
||||
: conditions?.availableSupply.toString() || "",
|
||||
: claimCondition?.availableSupply.toString() || "",
|
||||
maxClaimablePerWallet:
|
||||
conditions?.maxMintPerWallet.toString() === "0" ||
|
||||
conditions?.maxMintPerWallet.toString() === MAX_UINT_256
|
||||
claimCondition?.maxMintPerWallet.toString() === "0" ||
|
||||
claimCondition?.maxMintPerWallet.toString() === MAX_UINT_256
|
||||
? ""
|
||||
: conditions?.maxMintPerWallet.toString() || "",
|
||||
startTime: conditions?.startTimestamp
|
||||
? fromUnixTime(conditions?.startTimestamp)
|
||||
: claimCondition?.maxMintPerWallet.toString() || "",
|
||||
startTime: claimCondition?.startTimestamp
|
||||
? fromUnixTime(claimCondition?.startTimestamp)
|
||||
: defaultStartDate,
|
||||
endTime: conditions?.endTimestamp
|
||||
? fromUnixTime(conditions?.endTimestamp)
|
||||
endTime: claimCondition?.endTimestamp
|
||||
? fromUnixTime(claimCondition?.endTimestamp)
|
||||
: defaultEndDate,
|
||||
allowList: [],
|
||||
},
|
||||
@@ -474,39 +493,30 @@ function ClaimConditionSection(props: {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{!props.isErc721 && (
|
||||
<Form {...tokenIdForm}>
|
||||
<form>
|
||||
<FormField
|
||||
control={tokenIdForm.control}
|
||||
name="tokenId"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex-1">
|
||||
<FormLabel>Token ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} disabled={!props.isOwnerAccount} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</form>
|
||||
</Form>
|
||||
{props.noClaimConditionSet && !addClaimConditionButtonClicked && (
|
||||
<>
|
||||
<Alert variant="warning">
|
||||
<CircleAlertIcon className="size-5 max-sm:hidden" />
|
||||
<AlertTitle>No Claim Condition Set</AlertTitle>
|
||||
<AlertDescription>
|
||||
You have not set a claim condition for this token. You can set a
|
||||
claim condition by clicking the "Set Claim Condition" button.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<Button
|
||||
onClick={() => setAddClaimConditionButtonClicked(true)}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
Add Claim Condition
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
{!props.isErc721 &&
|
||||
tokenId !== "" &&
|
||||
BigInt(tokenId) >= 0n &&
|
||||
claimConditionErc1155Query.isPending && (
|
||||
<Skeleton className="h-[350px]" />
|
||||
)}
|
||||
|
||||
{(props.isErc721 ||
|
||||
(tokenId !== "" &&
|
||||
BigInt(tokenId) >= 0n &&
|
||||
!claimConditionErc1155Query.isPending)) && (
|
||||
{(!props.noClaimConditionSet || addClaimConditionButtonClicked) && (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)}>
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<FormField
|
||||
@@ -668,9 +678,9 @@ function ClaimConditionSection(props: {
|
||||
</TransactionButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</form>{" "}
|
||||
</Form>
|
||||
</form>{" "}
|
||||
</Form>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-1
@@ -166,7 +166,8 @@ export function CurrencySelector<
|
||||
))}
|
||||
{isCustomCurrency &&
|
||||
!isPaymentsSelector &&
|
||||
initialValue !== NATIVE_TOKEN_ADDRESS.toLowerCase() && (
|
||||
initialValue.toLowerCase() !==
|
||||
NATIVE_TOKEN_ADDRESS.toLowerCase() && (
|
||||
<SelectItem key={initialValue} value={initialValue}>
|
||||
{initialValue}
|
||||
</SelectItem>
|
||||
|
||||
+8
-11
@@ -27,12 +27,9 @@ import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
type PreparedTransaction,
|
||||
prepareContractCall,
|
||||
sendAndConfirmTransaction,
|
||||
} from "thirdweb";
|
||||
import { type PreparedTransaction, sendAndConfirmTransaction } from "thirdweb";
|
||||
import { MintableERC721, MintableERC1155 } from "thirdweb/modules";
|
||||
import { grantRoles, hasAllRoles } from "thirdweb/modules";
|
||||
import { useReadContract } from "thirdweb/react";
|
||||
import type { NFTMetadataInputLimited } from "types/modified-types";
|
||||
import { parseAttributes } from "utils/parseAttributes";
|
||||
@@ -79,10 +76,10 @@ function MintableModule(props: ModuleInstanceProps) {
|
||||
contract: contract,
|
||||
},
|
||||
);
|
||||
const hasMinterRole = useReadContract({
|
||||
const hasMinterRole = useReadContract(hasAllRoles, {
|
||||
contract: contract,
|
||||
method: "function hasAllRoles(address user, uint256 roles) returns (bool)",
|
||||
params: [ownerAccount?.address || "", MINTER_ROLE],
|
||||
user: ownerAccount?.address || "",
|
||||
roles: MINTER_ROLE,
|
||||
});
|
||||
|
||||
const isBatchMetadataInstalled = !!props.allModuleContractInfo.find(
|
||||
@@ -97,10 +94,10 @@ function MintableModule(props: ModuleInstanceProps) {
|
||||
}
|
||||
|
||||
if (!hasMinterRole.data) {
|
||||
const grantRoleTx = prepareContractCall({
|
||||
const grantRoleTx = grantRoles({
|
||||
contract,
|
||||
method: "function grantRole(address user, uint256 role) public",
|
||||
params: [ownerAccount.address || "", MINTER_ROLE],
|
||||
user: ownerAccount.address,
|
||||
roles: MINTER_ROLE,
|
||||
});
|
||||
|
||||
await sendAndConfirmTransaction({
|
||||
|
||||
+1
-1
@@ -103,7 +103,7 @@ function TransferableModule(props: ModuleInstanceProps) {
|
||||
<TransferableModuleUI
|
||||
{...props}
|
||||
isPending={isTransferEnabledQuery.isPending}
|
||||
isRestricted={!!isTransferEnabledQuery.data}
|
||||
isRestricted={!isTransferEnabledQuery.data}
|
||||
adminAddress={props.ownerAccount?.address || ""}
|
||||
update={update}
|
||||
isOwnerAccount={!!props.ownerAccount}
|
||||
|
||||
+22
-14
@@ -48,7 +48,7 @@ const testAddress1 = "0x1F846F6DAE38E1C88D71EAA191760B15f38B7A37";
|
||||
const claimCondition = {
|
||||
availableSupply: BigInt(100),
|
||||
maxMintPerWallet: BigInt(10),
|
||||
pricePerUnit: 10n,
|
||||
pricePerUnit: 10000000000000000000n,
|
||||
// we get checksummed NATIVE_TOKEN_ADDRESS from claim condition query for native token
|
||||
currency: checksumAddress(NATIVE_TOKEN_ADDRESS),
|
||||
// last week
|
||||
@@ -64,6 +64,8 @@ function Component() {
|
||||
const [isClaimConditionLoading, setIsClaimConditionLoading] = useState(false);
|
||||
const [isPrimarySaleRecipientLoading, setIsPrimarySaleRecipientLoading] =
|
||||
useState(false);
|
||||
const [tokenId, setTokenId] = useState("");
|
||||
const [noClaimConditionSet, setNoClaimConditionSet] = useState(false);
|
||||
|
||||
async function updatePrimarySaleRecipientStub(
|
||||
values: PrimarySaleRecipientFormValues,
|
||||
@@ -82,12 +84,6 @@ function Component() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
}
|
||||
|
||||
async function getClaimConditionErc1155Stub() {
|
||||
console.log("claim condition in stub: ", claimCondition);
|
||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
||||
return claimCondition;
|
||||
}
|
||||
|
||||
const removeMutation = useMutation({
|
||||
mutationFn: async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
@@ -142,6 +138,13 @@ function Component() {
|
||||
id="isPrimarySaleRecipientLoading"
|
||||
label="Primary Sale Recipient Section Loading"
|
||||
/>
|
||||
|
||||
<CheckboxWithLabel
|
||||
value={noClaimConditionSet}
|
||||
onChange={setNoClaimConditionSet}
|
||||
id="noClaimConditionSet"
|
||||
label="No Claim Condition Set"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ClaimableModuleUI
|
||||
@@ -156,14 +159,16 @@ function Component() {
|
||||
setPrimarySaleRecipient: updatePrimarySaleRecipientStub,
|
||||
}}
|
||||
claimConditionSection={{
|
||||
data: isClaimConditionLoading
|
||||
? undefined
|
||||
: {
|
||||
claimCondition,
|
||||
tokenDecimals: 18,
|
||||
},
|
||||
data:
|
||||
isClaimConditionLoading || (!isErc721 && !tokenId)
|
||||
? undefined
|
||||
: {
|
||||
claimCondition,
|
||||
tokenDecimals: 18,
|
||||
},
|
||||
isLoading: false,
|
||||
setClaimCondition: updateClaimConditionStub,
|
||||
getClaimConditionErc1155: getClaimConditionErc1155Stub,
|
||||
tokenId,
|
||||
}}
|
||||
mintSection={{
|
||||
mint: mintStub,
|
||||
@@ -175,6 +180,9 @@ function Component() {
|
||||
isOwnerAccount={isOwner}
|
||||
isErc721={isErc721}
|
||||
contractChainId={1}
|
||||
setTokenId={setTokenId}
|
||||
isValidTokenId={true}
|
||||
noClaimConditionSet={noClaimConditionSet}
|
||||
/>
|
||||
|
||||
<Toaster richColors />
|
||||
|
||||
+1
-1
@@ -32,7 +32,7 @@ import type { Account } from "thirdweb/wallets";
|
||||
import { ModuleInstance } from "./module-instance";
|
||||
import { useModuleContractInfo } from "./moduleContractInfo";
|
||||
|
||||
export type ModuleCardProps = {
|
||||
type ModuleCardProps = {
|
||||
moduleAddress: string;
|
||||
contract: ContractOptions;
|
||||
onRemoveModule: () => void;
|
||||
|
||||
+2
-2
@@ -31,7 +31,7 @@ export function ModuleInstance(props: ModuleInstanceProps) {
|
||||
return <TransferableModule {...props} />;
|
||||
}
|
||||
|
||||
if (props.contractInfo.name.includes("Mintable-ignore")) {
|
||||
if (props.contractInfo.name.includes("Mintable")) {
|
||||
return <MintableModule {...props} />;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function ModuleInstance(props: ModuleInstanceProps) {
|
||||
return <RoyaltyModule {...props} />;
|
||||
}
|
||||
|
||||
if (props.contractInfo.name.includes("Claimable-ignore")) {
|
||||
if (props.contractInfo.name.includes("Claimable")) {
|
||||
return <ClaimableModule {...props} />;
|
||||
}
|
||||
|
||||
|
||||
+7
-17
@@ -4,6 +4,7 @@ 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 { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
@@ -26,7 +27,8 @@ import type { 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 { Badge, Button, Card, CodeBlock, Heading, Text } from "tw-components";
|
||||
import {} from "tw-components";
|
||||
import { Button, Card, Heading, Text } from "tw-components";
|
||||
import { NFTMediaWithEmptyState } from "tw-components/nft-media";
|
||||
import { shortenString } from "utils/usedapp-external";
|
||||
import { NftProperty } from "../components/nft-property";
|
||||
@@ -158,12 +160,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
overflow={{ base: "auto", md: "hidden" }}
|
||||
pb={{ base: 4, md: 0 }}
|
||||
>
|
||||
<ButtonGroup
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
spacing={2}
|
||||
w={(tabs.length + 1) * 95}
|
||||
>
|
||||
<ButtonGroup size="sm" variant="ghost" spacing={2}>
|
||||
<Button
|
||||
type="button"
|
||||
isActive={tab === "Details"}
|
||||
@@ -241,11 +238,7 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
<GridItem colSpan={4}>
|
||||
<Heading size="label.md">Token Standard</Heading>
|
||||
</GridItem>
|
||||
<GridItem colSpan={8}>
|
||||
<Badge size="label.sm" variant="subtle">
|
||||
{nft.type}
|
||||
</Badge>
|
||||
</GridItem>
|
||||
<GridItem colSpan={8}>{nft.type}</GridItem>
|
||||
{nft.type !== "ERC721" && (
|
||||
<>
|
||||
<GridItem colSpan={4}>
|
||||
@@ -319,12 +312,9 @@ export const TokenIdPage: React.FC<TokenIdPageProps> = ({
|
||||
))}
|
||||
</SimpleGrid>
|
||||
) : (
|
||||
<CodeBlock
|
||||
<CodeClient
|
||||
code={JSON.stringify(properties, null, 2) || ""}
|
||||
language="json"
|
||||
canCopy={false}
|
||||
wrap={false}
|
||||
overflow="auto"
|
||||
lang="json"
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
+3
-5
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -17,7 +18,6 @@ import type { ThirdwebContract } from "thirdweb";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { useReadContract, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { Button } from "tw-components";
|
||||
|
||||
interface BatchLazyMintButtonProps {
|
||||
canCreateDelayedRevealBatch: boolean;
|
||||
@@ -55,10 +55,8 @@ export const BatchLazyMintButton: React.FC<BatchLazyMintButtonProps> = ({
|
||||
<MinterOnly contract={contract}>
|
||||
<Sheet onOpenChange={setOpen} open={open}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<FileStackIcon className="size-4" />}
|
||||
>
|
||||
<Button variant="primary" className="gap-2">
|
||||
<FileStackIcon className="size-4" />
|
||||
Batch Upload
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
|
||||
+3
-3
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -18,7 +19,6 @@ import { type ThirdwebContract, ZERO_ADDRESS } from "thirdweb";
|
||||
import { getApprovalForTransaction } from "thirdweb/extensions/erc20";
|
||||
import { claimTo } from "thirdweb/extensions/erc721";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { Button } from "tw-components";
|
||||
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
|
||||
|
||||
const CLAIM_FORM_ID = "nft-claim-form";
|
||||
@@ -45,8 +45,8 @@ export const NFTClaimButton: React.FC<NFTClaimButtonProps> = ({ contract }) => {
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button colorScheme="primary" leftIcon={<GemIcon className="size-4" />}>
|
||||
Claim
|
||||
<Button variant="primary" className="gap-2">
|
||||
<GemIcon className="size-4" /> Claim
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="overflow-y-auto sm:w-[540px] sm:max-w-[90%] lg:w-[700px]">
|
||||
|
||||
+3
-6
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -10,7 +11,6 @@ import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Button } from "tw-components";
|
||||
import { LazyMintNftForm } from "./lazy-mint-form";
|
||||
|
||||
interface NFTLazyMintButtonProps {
|
||||
@@ -28,11 +28,8 @@ export const NFTLazyMintButton: React.FC<NFTLazyMintButtonProps> = ({
|
||||
<MinterOnly contract={contract}>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
>
|
||||
<Button variant="primary" className="gap-2" {...restButtonProps}>
|
||||
<PlusIcon className="size-5" />
|
||||
Single Upload
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
|
||||
+3
-3
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionButton,
|
||||
@@ -25,7 +26,6 @@ import { lazyMint as lazyMint721 } from "thirdweb/extensions/erc721";
|
||||
import { lazyMint as lazyMint1155 } from "thirdweb/extensions/erc1155";
|
||||
import { useActiveAccount, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import {
|
||||
Button,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
@@ -313,9 +313,9 @@ export const LazyMintNftForm: React.FC<LazyMintNftFormParams> = ({
|
||||
</form>
|
||||
<div className="mt-8 flex flex-row justify-end gap-3">
|
||||
<Button
|
||||
isDisabled={sendAndConfirmTx.isPending}
|
||||
disabled={sendAndConfirmTx.isPending}
|
||||
variant="outline"
|
||||
mr={3}
|
||||
className="mr-3"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
|
||||
+3
-6
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -11,7 +12,6 @@ import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Button } from "tw-components";
|
||||
import { NFTMintForm } from "./mint-form";
|
||||
|
||||
interface NFTMintButtonProps {
|
||||
@@ -30,11 +30,8 @@ export const NFTMintButton: React.FC<NFTMintButtonProps> = ({
|
||||
<MinterOnly contract={contract}>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
>
|
||||
<Button variant="primary" className="gap-2" {...restButtonProps}>
|
||||
<PlusIcon className="size-5" />
|
||||
Mint
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
|
||||
+4
-6
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -18,7 +19,7 @@ import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { getBatchesToReveal, reveal } from "thirdweb/extensions/erc721";
|
||||
import { useReadContract, useSendAndConfirmTransaction } from "thirdweb/react";
|
||||
import { Button, FormErrorMessage, FormLabel } from "tw-components";
|
||||
import { FormErrorMessage, FormLabel } from "tw-components";
|
||||
|
||||
interface NFTRevealButtonProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -48,11 +49,8 @@ export const NFTRevealButton: React.FC<NFTRevealButtonProps> = ({
|
||||
<MinterOnly contract={contract}>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<EyeIcon className="size-4" />}
|
||||
>
|
||||
Reveal NFTs
|
||||
<Button variant="primary" className="gap-2">
|
||||
<EyeIcon className="size-4" /> Reveal NFTs
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
|
||||
+3
-7
@@ -1,4 +1,5 @@
|
||||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -10,7 +11,6 @@ import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Button } from "tw-components";
|
||||
import { SharedMetadataForm } from "./shared-metadata-form";
|
||||
|
||||
interface NFTSharedMetadataButtonProps {
|
||||
@@ -25,12 +25,8 @@ export const NFTSharedMetadataButton: React.FC<
|
||||
<MinterOnly contract={contract}>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<PlusIcon className="size-5" />}
|
||||
{...restButtonProps}
|
||||
>
|
||||
Set NFT Metadata
|
||||
<Button variant="primary" className="gap-2" {...restButtonProps}>
|
||||
<PlusIcon className="size-5" /> Set NFT Metadata
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
|
||||
+10
-8
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card } from "@/components/ui/card";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useResolveContractAbi } from "@3rdweb-sdk/react/hooks/useResolveContractAbi";
|
||||
import {
|
||||
@@ -21,7 +24,7 @@ import { CircleCheckIcon, CircleXIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Badge, Button, Card, Heading } from "tw-components";
|
||||
import { Heading } from "tw-components";
|
||||
|
||||
interface ContractSourcesPageProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -91,9 +94,7 @@ const VerifyContractModal: React.FC<
|
||||
<ModalHeader>
|
||||
<Flex gap={2} align="center">
|
||||
<Heading size="subtitle.md">Contract Verification</Heading>
|
||||
<Badge variant="outline" colorScheme="purple" rounded="md" px={2}>
|
||||
beta
|
||||
</Badge>
|
||||
<Badge>beta</Badge>
|
||||
</Flex>
|
||||
</ModalHeader>
|
||||
<ModalCloseButton mt={2} />
|
||||
@@ -213,11 +214,11 @@ export const ContractSourcesPage: React.FC<ContractSourcesPageProps> = ({
|
||||
chainId={contract.chain.id}
|
||||
contractAddress={contract.address}
|
||||
/>
|
||||
<Button variant="solid" colorScheme="purple" onClick={onOpen}>
|
||||
<Button variant="primary" onClick={onOpen}>
|
||||
Verify contract
|
||||
</Button>
|
||||
</Flex>
|
||||
<Card p={0}>
|
||||
<Card>
|
||||
<SourcesPanel sources={sources} abi={abiQuery.data} />
|
||||
</Card>
|
||||
</Flex>
|
||||
@@ -264,7 +265,7 @@ function RefreshContractMetadataButton(props: {
|
||||
|
||||
return (
|
||||
<Button
|
||||
isLoading={contractCacheMutation.isPending}
|
||||
disabled={contractCacheMutation.isPending}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
toast.promise(contractCacheMutation.mutateAsync(), {
|
||||
@@ -274,8 +275,9 @@ function RefreshContractMetadataButton(props: {
|
||||
error: (e) => e?.message || "Failed to refresh contract data.",
|
||||
});
|
||||
}}
|
||||
className="w-[182px]"
|
||||
>
|
||||
Refresh Contract Data
|
||||
{contractCacheMutation.isPending ? <Spinner /> : "Refresh Contract Data"}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useSplitDistributeFunds } from "@3rdweb-sdk/react/hooks/useSplit";
|
||||
import { MismatchButton } from "components/buttons/MismatchButton";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
@@ -7,7 +8,6 @@ import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useTxNotifications } from "hooks/useTxNotifications";
|
||||
import { useMemo } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { Button } from "tw-components";
|
||||
import type { Balance } from "../ContractSplitPage";
|
||||
|
||||
interface DistributeButtonProps {
|
||||
@@ -90,7 +90,7 @@ export const DistributeButton: React.FC<DistributeButtonProps> = ({
|
||||
|
||||
if (numTransactions === 0) {
|
||||
return (
|
||||
<Button isDisabled colorScheme="primary" {...restButtonProps}>
|
||||
<Button disabled variant="primary" {...restButtonProps}>
|
||||
Nothing to distribute
|
||||
</Button>
|
||||
);
|
||||
|
||||
+6
-6
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -7,12 +8,11 @@ import {
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import { Droplet } from "lucide-react";
|
||||
import { DropletIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebContract } from "thirdweb";
|
||||
import { balanceOf } from "thirdweb/extensions/erc20";
|
||||
import { useActiveAccount, useReadContract } from "thirdweb/react";
|
||||
import { Button } from "tw-components";
|
||||
import { TokenAirdropForm } from "./airdrop-form";
|
||||
interface TokenAirdropButtonProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -33,12 +33,12 @@ export const TokenAirdropButton: React.FC<TokenAirdropButtonProps> = ({
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<Droplet size={16} />}
|
||||
variant="primary"
|
||||
{...restButtonProps}
|
||||
isDisabled={!hasBalance}
|
||||
disabled={!hasBalance}
|
||||
className="gap-2"
|
||||
>
|
||||
Airdrop
|
||||
<DropletIcon size={16} /> Airdrop
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
|
||||
|
||||
+5
-5
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -23,7 +24,6 @@ import {
|
||||
useSendAndConfirmTransaction,
|
||||
} from "thirdweb/react";
|
||||
import {
|
||||
Button,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
@@ -59,12 +59,12 @@ export const TokenBurnButton: React.FC<TokenBurnButtonProps> = ({
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<Flame size={16} />}
|
||||
variant="primary"
|
||||
{...restButtonProps}
|
||||
isDisabled={!hasBalance}
|
||||
disabled={!hasBalance}
|
||||
className="gap-2"
|
||||
>
|
||||
Burn
|
||||
<Flame size={16} /> Burn
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000]">
|
||||
|
||||
+5
-13
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -11,7 +12,7 @@ import {
|
||||
import { FormControl, Input } from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { Gem } from "lucide-react";
|
||||
import { GemIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -22,12 +23,7 @@ import {
|
||||
useReadContract,
|
||||
useSendAndConfirmTransaction,
|
||||
} from "thirdweb/react";
|
||||
import {
|
||||
Button,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
} from "tw-components";
|
||||
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
|
||||
|
||||
interface TokenClaimButtonProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -52,12 +48,8 @@ export const TokenClaimButton: React.FC<TokenClaimButtonProps> = ({
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<Gem size={16} />}
|
||||
{...restButtonProps}
|
||||
>
|
||||
Claim
|
||||
<Button variant="primary" className="gap-2" {...restButtonProps}>
|
||||
<GemIcon size={16} /> Claim
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000]">
|
||||
|
||||
+5
-8
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -12,7 +13,7 @@ import { MinterOnly } from "@3rdweb-sdk/react/components/roles/minter-only";
|
||||
import { FormControl, Input } from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { Plus } from "lucide-react";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -23,7 +24,7 @@ import {
|
||||
useReadContract,
|
||||
useSendAndConfirmTransaction,
|
||||
} from "thirdweb/react";
|
||||
import { Button, FormErrorMessage, FormLabel } from "tw-components";
|
||||
import { FormErrorMessage, FormLabel } from "tw-components";
|
||||
|
||||
interface TokenMintButtonProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -50,12 +51,8 @@ export const TokenMintButton: React.FC<TokenMintButtonProps> = ({
|
||||
<MinterOnly contract={contract}>
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<Plus size={16} />}
|
||||
{...restButtonProps}
|
||||
>
|
||||
Mint
|
||||
<Button variant="primary" {...restButtonProps} className="gap-2">
|
||||
<PlusIcon size={16} /> Mint
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000]">
|
||||
|
||||
+7
-11
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -12,7 +13,7 @@ import { FormControl, Input } from "@chakra-ui/react";
|
||||
import { TransactionButton } from "components/buttons/TransactionButton";
|
||||
import { SolidityInput } from "contract-ui/components/solidity-inputs";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { Send } from "lucide-react";
|
||||
import { SendIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
@@ -23,12 +24,7 @@ import {
|
||||
useReadContract,
|
||||
useSendAndConfirmTransaction,
|
||||
} from "thirdweb/react";
|
||||
import {
|
||||
Button,
|
||||
FormErrorMessage,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
} from "tw-components";
|
||||
import { FormErrorMessage, FormHelperText, FormLabel } from "tw-components";
|
||||
|
||||
interface TokenTransferButtonProps {
|
||||
contract: ThirdwebContract;
|
||||
@@ -57,12 +53,12 @@ export const TokenTransferButton: React.FC<TokenTransferButtonProps> = ({
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
colorScheme="primary"
|
||||
leftIcon={<Send size={16} />}
|
||||
variant="primary"
|
||||
{...restButtonProps}
|
||||
isDisabled={!hasBalance}
|
||||
disabled={!hasBalance}
|
||||
className="gap-2"
|
||||
>
|
||||
Transfer
|
||||
<SendIcon size={16} /> Transfer
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="z-[10000]">
|
||||
|
||||
+4
-1
@@ -39,7 +39,7 @@ export async function ChainListRow({
|
||||
}: ChainListRowProps) {
|
||||
const chainMetadata = await getChainMetadata(chainId);
|
||||
return (
|
||||
<TableRow className="relative hover:bg-muted/50">
|
||||
<TableRow linkBox className="hover:bg-muted/50">
|
||||
<TableCell>{favoriteButton}</TableCell>
|
||||
{/* Name */}
|
||||
<TableCell>
|
||||
@@ -122,6 +122,9 @@ function pidToHref(pid: (typeof products)[number]["id"]) {
|
||||
case "rpc-edge": {
|
||||
return "https://portal.thirdweb.com/infrastructure/rpc-edge/overview";
|
||||
}
|
||||
case "insight": {
|
||||
return "https://portal.thirdweb.com/insight";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
export function InsightIcon(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={props.className}
|
||||
>
|
||||
<title>Insight</title>
|
||||
<circle
|
||||
cx="8.11963"
|
||||
cy="12.9889"
|
||||
r="4.505"
|
||||
fill="url(#paint0_linear_1095_479)"
|
||||
/>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M21.2163 7.97235C21.2163 7.96914 21.2164 7.96592 21.2164 7.96271C21.2164 5.60751 18.4863 3.69824 15.1186 3.69824C11.7509 3.69824 9.02089 5.60751 9.02089 7.96271C9.02089 7.96324 9.02089 7.96378 9.02089 7.96431H9.02087V17.0369C9.02087 17.0372 9.02087 17.0374 9.02087 17.0376C9.02137 19.3925 11.7512 21.3014 15.1186 21.3014C18.486 21.3014 21.2158 19.3925 21.2163 17.0376C21.2163 17.0374 21.2163 17.0372 21.2163 17.0369V7.97235Z"
|
||||
fill="#06B6D4"
|
||||
/>
|
||||
<path
|
||||
d="M11.1232 10.3516V10.3516C13.5336 11.9944 16.704 11.9944 19.1145 10.3515V10.3515"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M11.1232 15.0107V15.0107C13.5336 16.6536 16.704 16.6535 19.1145 15.0107V15.0107"
|
||||
stroke="white"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_1095_479"
|
||||
x1="8.11963"
|
||||
y1="16.3861"
|
||||
x2="-1.14166"
|
||||
y2="3.67577"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stopColor="#155E75" />
|
||||
<stop offset="1" stopColor="#27B0DB" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { ChainSupportedService } from "../../types/chain";
|
||||
import { ConnectSDKIcon } from "./icons/ConnectSDKIcon";
|
||||
import { ContractIcon } from "./icons/ContractIcon";
|
||||
import { EngineIcon } from "./icons/EngineIcon";
|
||||
import { InsightIcon } from "./icons/InsightIcon";
|
||||
import { PayIcon } from "./icons/PayIcon";
|
||||
import { RPCIcon } from "./icons/RPCIcon";
|
||||
import { SmartAccountIcon } from "./icons/SmartAccountIcon";
|
||||
@@ -49,6 +50,13 @@ export const products = [
|
||||
description: "Point of sale solution for bridging, onramping & swapping",
|
||||
link: "https://portal.thirdweb.com/connect/pay/overview",
|
||||
},
|
||||
{
|
||||
name: "Insight",
|
||||
id: "insight",
|
||||
icon: InsightIcon,
|
||||
description: "Query, transform and analyze blockchain data",
|
||||
link: "https://portal.thirdweb.com/insight",
|
||||
},
|
||||
] satisfies Array<{
|
||||
name: string;
|
||||
id: ChainSupportedService;
|
||||
|
||||
@@ -7,7 +7,8 @@ export type ChainSupportedService =
|
||||
| "account-abstraction"
|
||||
| "pay"
|
||||
| "rpc-edge"
|
||||
| "chainsaw";
|
||||
| "chainsaw"
|
||||
| "insight";
|
||||
|
||||
export type ChainService = {
|
||||
service: ChainSupportedService;
|
||||
|
||||
@@ -109,7 +109,7 @@ const OP_CTA = {
|
||||
description:
|
||||
"Successful applicants receive gas grants for use across all supported Optimism Superchain networks. These grants can sponsor gas fees for any onchain activity using our Account Abstraction tools.",
|
||||
buttonText: "Apply now",
|
||||
buttonLink: "/dashboard/settings/gas-credits",
|
||||
buttonLink: "/team/~/~/settings/credits",
|
||||
} satisfies ChainCTAProps;
|
||||
|
||||
const chainMetaRecord = {
|
||||
|
||||
+2
-1
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import { TableContainer } from "@/components/ui/table";
|
||||
import {
|
||||
@@ -15,7 +17,6 @@ import { useTrack } from "hooks/analytics/useTrack";
|
||||
import { useRpcValidation } from "hooks/chains/useRpcValidation";
|
||||
import { AlertCircleIcon, CheckCircleIcon } from "lucide-react";
|
||||
import { type ChangeEvent, useState } from "react";
|
||||
|
||||
import { Button, Card, FormLabel, Link } from "tw-components";
|
||||
|
||||
const StatusCheck = (props: {
|
||||
@@ -0,0 +1,41 @@
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import ChainValidation from "./ChainValidation";
|
||||
|
||||
const title = "Validate Chain";
|
||||
const description = "Validate a given chain is compatible with thirdweb.";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title,
|
||||
description,
|
||||
robots: "noindex, nofollow",
|
||||
};
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<ChakraProviderSetup>
|
||||
<div className="container py-6">
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-2">
|
||||
<h1 className="font-semibold text-2xl tracking-tight">
|
||||
Validate Chain
|
||||
</h1>
|
||||
<p>
|
||||
Validate a given chain is compatible with{" "}
|
||||
<Link
|
||||
href="/chainlist"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
thirdweb
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<ChainValidation />
|
||||
</div>
|
||||
</div>
|
||||
</ChakraProviderSetup>
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user