Compare commits
39
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -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."
|
||||
|
||||
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>
|
||||
);
|
||||
|
||||
@@ -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,39 @@ 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 = {
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
@@ -117,7 +131,7 @@ const moduleExports = {
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
return redirects();
|
||||
return getRedirects();
|
||||
},
|
||||
async rewrites() {
|
||||
return [
|
||||
@@ -151,74 +165,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();
|
||||
@@ -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",
|
||||
@@ -50,7 +50,7 @@
|
||||
"@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.59.20",
|
||||
"@tanstack/react-table": "^8.17.3",
|
||||
"@thirdweb-dev/service-utils": "workspace:*",
|
||||
"@vercel/functions": "^1.4.2",
|
||||
@@ -70,7 +70,7 @@
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"lucide-react": "0.454.0",
|
||||
"next": "15.0.2",
|
||||
"next": "15.0.3",
|
||||
"next-plausible": "^3.12.3",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-themes": "^0.4.3",
|
||||
@@ -78,8 +78,6 @@
|
||||
"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",
|
||||
@@ -94,6 +92,7 @@
|
||||
"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,8 +106,8 @@
|
||||
"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",
|
||||
@@ -137,7 +136,7 @@
|
||||
"eslint-config-biome": "1.9.3",
|
||||
"eslint-plugin-react-compiler": "19.0.0-beta-8a03594-20241020",
|
||||
"eslint-plugin-storybook": "^0.9.0",
|
||||
"knip": "5.36.2",
|
||||
"knip": "5.36.3",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.47",
|
||||
"storybook": "8.4.2",
|
||||
|
||||
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,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[];
|
||||
}
|
||||
@@ -13,13 +13,8 @@ export type Team = {
|
||||
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;
|
||||
billingStatus: "validPayment" | (string & {}) | null;
|
||||
billingEmail: string | null;
|
||||
};
|
||||
|
||||
export async function getTeamBySlug(slug: string) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -13,7 +13,7 @@ import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { Badge } from "./badge";
|
||||
|
||||
const meta = {
|
||||
title: "Shadcn/table",
|
||||
title: "Shadcn/Table",
|
||||
component: Component,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
|
||||
@@ -197,6 +197,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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,15 @@ import {
|
||||
THIRDWEB_STORAGE_DOMAIN,
|
||||
} from "constants/urls";
|
||||
import { createThirdwebClient } from "thirdweb";
|
||||
import { setThirdwebDomains } from "thirdweb/utils";
|
||||
import { getChainMetadata } from "thirdweb/chains";
|
||||
import { populateEip712Transaction } from "thirdweb/transaction";
|
||||
import {
|
||||
getTransactionDecorator,
|
||||
isZkSyncChain,
|
||||
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 +33,54 @@ 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 }) => {
|
||||
// use paymaster for zk chains on testnets
|
||||
const chainMeta = await getChainMetadata(transaction.chain);
|
||||
if (chainMeta.testnet) {
|
||||
const isZkChain = await isZkSyncChain(transaction.chain);
|
||||
if (isZkChain) {
|
||||
const serializedTx = await populateEip712Transaction({
|
||||
transaction,
|
||||
account,
|
||||
});
|
||||
const pmData = await getZkPaymasterData({
|
||||
options: {
|
||||
client: transaction.client,
|
||||
chain: transaction.chain,
|
||||
},
|
||||
transaction: serializedTx,
|
||||
}).catch((e) => {
|
||||
console.warn(
|
||||
"No zk paymaster data available on chain ",
|
||||
transaction.chain.id,
|
||||
e,
|
||||
);
|
||||
return undefined;
|
||||
});
|
||||
return {
|
||||
account,
|
||||
transaction: {
|
||||
...transaction,
|
||||
eip712: pmData
|
||||
? {
|
||||
...transaction.eip712,
|
||||
paymaster: pmData.paymaster,
|
||||
paymasterInput: pmData.paymasterInput,
|
||||
}
|
||||
: transaction.eip712,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
return { account, transaction };
|
||||
});
|
||||
}
|
||||
|
||||
return createThirdwebClient({
|
||||
secretKey: jwt ? jwt : DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
clientId: DASHBOARD_THIRDWEB_CLIENT_ID,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,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 +35,7 @@ export const CustomConnectWallet = (props: {
|
||||
connectButtonClassName?: string;
|
||||
signInLinkButtonClassName?: string;
|
||||
detailsButtonClassName?: string;
|
||||
chain?: Chain;
|
||||
}) => {
|
||||
const thirdwebClient = useThirdwebClient();
|
||||
const loginRequired =
|
||||
@@ -126,8 +126,6 @@ export const CustomConnectWallet = (props: {
|
||||
<div className="flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted">
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{/* need auto connect here so that we actually connect */}
|
||||
<AutoConnect client={thirdwebClient} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -147,8 +145,6 @@ export const CustomConnectWallet = (props: {
|
||||
Sign In
|
||||
</Link>
|
||||
</Button>
|
||||
{/* need auto connect here so that we actually connect */}
|
||||
<AutoConnect client={thirdwebClient} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -209,6 +205,7 @@ export const CustomConnectWallet = (props: {
|
||||
},
|
||||
},
|
||||
}}
|
||||
chain={props.chain}
|
||||
/>
|
||||
|
||||
<LazyConfigureNetworkModal
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { UserOpStats } from "@/api/analytics";
|
||||
import {
|
||||
type Query,
|
||||
useMutation,
|
||||
@@ -231,36 +232,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;
|
||||
@@ -373,43 +344,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 +381,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 +464,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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
+5
-8
@@ -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";
|
||||
@@ -204,7 +202,10 @@ export function FaucetButton({
|
||||
}
|
||||
|
||||
// Email verification is required to claim from the faucet
|
||||
if (accountQuery.data.status === "noCustomer") {
|
||||
if (
|
||||
!accountQuery.data.emailConfirmedAt &&
|
||||
!accountQuery.data.unconfirmedEmail
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
@@ -215,11 +216,7 @@ export function FaucetButton({
|
||||
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>
|
||||
)}
|
||||
{showOnboarding && <Onboarding onOpenChange={setShowOnBoarding} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
+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}
|
||||
|
||||
+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 />}
|
||||
|
||||
+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]">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { getThirdwebClient } from "@/constants/thirdweb.server";
|
||||
import { notFound } from "next/navigation";
|
||||
import { fetchDeployMetadata } from "thirdweb/contract";
|
||||
import { DeployContractInfo } from "../../../published-contract/components/contract-info";
|
||||
import { DeployFormForUri } from "../../../published-contract/components/uri-based-deploy";
|
||||
@@ -16,7 +17,12 @@ export default async function DirectDeployPage(props: DirectDeployPageProps) {
|
||||
client: getThirdwebClient(),
|
||||
// force `ipfs://` prefix
|
||||
uri: parsedUri.startsWith("ipfs://") ? parsedUri : `ipfs://${parsedUri}`,
|
||||
});
|
||||
}).catch(() => null);
|
||||
|
||||
if (!metadata) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container flex flex-col gap-4 py-8">
|
||||
<DeployContractInfo
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
import { DeployableContractTable } from "components/contract-components/contract-table";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default async function DeployMultipleContractsPage(props: {
|
||||
searchParams?: Promise<{
|
||||
ipfs?: string[] | string;
|
||||
}>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
const ipfsHashes = searchParams?.ipfs;
|
||||
|
||||
if (!ipfsHashes || !Array.isArray(ipfsHashes) || ipfsHashes.length === 0) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container flex grow flex-col py-10">
|
||||
<h1 className="mb-1 font-semibold text-2xl tracking-tight">
|
||||
Deploy Contracts
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Welcome to the thirdweb contract deployment flow.{" "}
|
||||
<Link
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
target="_blank"
|
||||
href="https://portal.thirdweb.com/contracts/deploy/overview"
|
||||
>
|
||||
Learn more about deploying your contracts.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<Suspense fallback={<GenericLoadingPage />}>
|
||||
<DeployableContractTable contractIds={ipfsHashes} context="deploy" />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -25,7 +25,11 @@ export default async function PublishContractPage(
|
||||
const publishMetadataFromUri = await fetchDeployMetadata({
|
||||
uri: publishUri,
|
||||
client: getThirdwebClient(),
|
||||
});
|
||||
}).catch(() => null);
|
||||
|
||||
if (!publishMetadataFromUri) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
let publishMetadata = publishMetadataFromUri;
|
||||
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { GenericLoadingPage } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
import { DeployableContractTable } from "components/contract-components/contract-table";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default async function PublishMultipleContractsPage(props: {
|
||||
searchParams?: Promise<{
|
||||
ipfs?: string[] | string;
|
||||
}>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
const ipfsHashes = searchParams?.ipfs;
|
||||
|
||||
if (!ipfsHashes || !Array.isArray(ipfsHashes) || ipfsHashes.length === 0) {
|
||||
notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container flex grow flex-col py-10">
|
||||
<h1 className="mb-1 font-semibold text-2xl tracking-tight">
|
||||
Publish Contracts
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Welcome to the thirdweb contract publish flow.{" "}
|
||||
<Link
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
target="_blank"
|
||||
href="https://portal.thirdweb.com/contracts/publish/overview"
|
||||
>
|
||||
Learn more about publishing your contracts.
|
||||
</Link>
|
||||
</p>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<Suspense fallback={<GenericLoadingPage />}>
|
||||
<DeployableContractTable contractIds={ipfsHashes} context="publish" />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { SidebarLayout } from "@/components/blocks/SidebarLayout";
|
||||
|
||||
export function ConnectSidebarLayout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<SidebarLayout
|
||||
sidebarLinks={[
|
||||
{
|
||||
label: "Analytics",
|
||||
href: "/dashboard/connect/analytics",
|
||||
},
|
||||
{
|
||||
label: "In-App Wallets",
|
||||
href: "/dashboard/connect/in-app-wallets",
|
||||
},
|
||||
{
|
||||
label: "Ecosystem Wallets",
|
||||
href: "/dashboard/connect/ecosystem",
|
||||
},
|
||||
{
|
||||
label: "Account Abstraction",
|
||||
href: "/dashboard/connect/account-abstraction",
|
||||
},
|
||||
{
|
||||
label: "Pay",
|
||||
href: "/dashboard/connect/pay",
|
||||
},
|
||||
{
|
||||
label: "Playground",
|
||||
href: "https://playground.thirdweb.com/connect/sign-in/button",
|
||||
},
|
||||
]}
|
||||
>
|
||||
{props.children}
|
||||
</SidebarLayout>
|
||||
);
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { ApiKeysMenu } from "../../../../../components/settings/ApiKeys/Menu";
|
||||
|
||||
export function AccountAbstractionAPIKeysMenu(props: {
|
||||
apiKeys: Pick<ApiKey, "name" | "key">[];
|
||||
selectedAPIKey: Pick<ApiKey, "name" | "key">;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
return (
|
||||
<ApiKeysMenu
|
||||
apiKeys={props.apiKeys}
|
||||
selectedKey={props.selectedAPIKey}
|
||||
onSelect={(key) => {
|
||||
router.push(`/dashboard/connect/account-abstraction/${key.key}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CircleAlertIcon } from "lucide-react";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import { isOpChainId } from "../../../../team/[team_slug]/[project_slug]/connect/account-abstraction/isOpChain";
|
||||
|
||||
export function OpChainAlert() {
|
||||
const chain = useActiveWalletChain();
|
||||
const isOpChain = chain?.id ? isOpChainId(chain.id) : false;
|
||||
|
||||
if (!isOpChain) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Alert variant="info">
|
||||
<CircleAlertIcon className="size-4" />
|
||||
<AlertTitle>Using the gas credits for OP chain paymaster</AlertTitle>
|
||||
<AlertDescription>
|
||||
Credits will automatically be applied to cover gas fees for any onchain
|
||||
activity across thirdweb services. <br />
|
||||
Eligible chains: OP Mainnet, Base, Zora, Frax, Mode.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
-25
@@ -1,25 +0,0 @@
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
|
||||
export function PageHeader() {
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<h1 className="font-semibold text-2xl tracking-tight lg:text-3xl">
|
||||
Account Abstraction
|
||||
</h1>
|
||||
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Easily integrate Account abstraction (ERC-4337) compliant smart accounts
|
||||
into your apps.{" "}
|
||||
<TrackedLinkTW
|
||||
target="_blank"
|
||||
label="docs-wallets"
|
||||
category="smart-wallet"
|
||||
href="https://portal.thirdweb.com/wallets/smart-wallet"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
View Documentation
|
||||
</TrackedLinkTW>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-79
@@ -1,79 +0,0 @@
|
||||
import { accountStatus } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { SmartWalletsBillingAlert } from "components/settings/ApiKeys/Alerts";
|
||||
import { ConnectSDKCard } from "components/shared/ConnectSDKCard";
|
||||
import { SmartWallets } from "components/smart-wallets";
|
||||
import { redirect } from "next/navigation";
|
||||
import { getAccount } from "../../../../../account/settings/getAccount";
|
||||
import { AccountAbstractionAPIKeysMenu } from "../AccountAbstractionAPIKeysMenu";
|
||||
import { OpChainAlert } from "../OpChainAlert";
|
||||
import { PageHeader } from "../PageHeader";
|
||||
import { getAASupportedAPIKeys } from "../getAASupportedAPIKeys";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
clientId: string;
|
||||
}>;
|
||||
searchParams: Promise<{
|
||||
tab?: string;
|
||||
}>;
|
||||
}) {
|
||||
const { clientId } = await props.params;
|
||||
const dashboardAccount = await getAccount();
|
||||
|
||||
if (!dashboardAccount) {
|
||||
redirect(
|
||||
`/login?next=${encodeURIComponent(
|
||||
`/dashboard/connect/account-abstraction/${clientId}`,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeys = await getAASupportedAPIKeys();
|
||||
const apiKey = apiKeys.find((key) => key.key === clientId);
|
||||
|
||||
if (!apiKey) {
|
||||
redirect("/dashboard/connect/account-abstraction");
|
||||
}
|
||||
|
||||
const hasSmartWalletsWithoutBilling = apiKeys.find((k) =>
|
||||
k.services?.find(
|
||||
(s) =>
|
||||
dashboardAccount.status !== accountStatus.validPayment &&
|
||||
s.name === "bundler",
|
||||
),
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col content-start justify-between gap-4 lg:flex-row">
|
||||
<PageHeader />
|
||||
|
||||
<div>
|
||||
<AccountAbstractionAPIKeysMenu
|
||||
apiKeys={apiKeys.map((x) => ({
|
||||
name: x.name,
|
||||
key: x.key,
|
||||
}))}
|
||||
selectedAPIKey={apiKey}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{hasSmartWalletsWithoutBilling ? (
|
||||
<SmartWalletsBillingAlert />
|
||||
) : (
|
||||
<OpChainAlert />
|
||||
)}
|
||||
<SmartWallets
|
||||
apiKeyServices={apiKey.services || []}
|
||||
trackingCategory="smart-wallet"
|
||||
tab={(await props.searchParams).tab}
|
||||
smartWalletsLayoutSlug={`/dashboard/connect/account-abstraction/${clientId}`}
|
||||
clientId={apiKey.key}
|
||||
/>
|
||||
<ConnectSDKCard
|
||||
title="Get Started"
|
||||
description="Add account abstraction to your app with the Connect SDK."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
import { getApiKeys } from "../../../../api/lib/getAPIKeys";
|
||||
|
||||
export async function getAASupportedAPIKeys() {
|
||||
return (await getApiKeys()).filter((key) => {
|
||||
return !!(key.services || []).find((srv) => srv.name === "bundler");
|
||||
});
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { ChakraProviderSetup } from "@/components/ChakraProviderSetup";
|
||||
import type { Metadata } from "next";
|
||||
import { getAbsoluteUrl } from "../../../../../lib/vercel-utils";
|
||||
|
||||
export default function Layout(props: { children: React.ReactNode }) {
|
||||
return <ChakraProviderSetup>{props.children}</ChakraProviderSetup>;
|
||||
}
|
||||
|
||||
const seo = {
|
||||
title: "The Complete Account Abstraction Toolkit | thirdweb",
|
||||
desc: "Add account abstraction to your web3 app & unlock powerful features for seamless onboarding, customizable transactions, & maximum security. Get started.",
|
||||
};
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: seo.title,
|
||||
description: seo.desc,
|
||||
openGraph: {
|
||||
title: seo.title,
|
||||
description: seo.desc,
|
||||
images: [
|
||||
{
|
||||
url: `${getAbsoluteUrl()}/assets/og-image/dashboard-wallets-smart-wallet.png`,
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: seo.title,
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -1,36 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { NoApiKeys } from "../../../../../components/settings/ApiKeys/NoApiKeys";
|
||||
import { ConnectSDKCard } from "../../../../../components/shared/ConnectSDKCard";
|
||||
import { getAuthToken } from "../../../../api/lib/getAuthToken";
|
||||
import { PageHeader } from "./PageHeader";
|
||||
import { getAASupportedAPIKeys } from "./getAASupportedAPIKeys";
|
||||
|
||||
export default async function Page() {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
redirect(
|
||||
`/login?next=${encodeURIComponent("/dashboard/connect/account-abstraction")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeys = await getAASupportedAPIKeys();
|
||||
const firstKey = apiKeys[0];
|
||||
|
||||
if (firstKey) {
|
||||
redirect(`/dashboard/connect/account-abstraction/${firstKey.key}`);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PageHeader />
|
||||
<div className="h-8" />
|
||||
<NoApiKeys service="Account Abstraction" />
|
||||
<div className="h-10" />
|
||||
<ConnectSDKCard
|
||||
title="Get Started"
|
||||
description="Add account abstraction to your app with the Connect SDK."
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-21
@@ -1,21 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ApiKey } from "@3rdweb-sdk/react/hooks/useApi";
|
||||
import { useDashboardRouter } from "../../../../../../@/lib/DashboardRouter";
|
||||
import { ApiKeysMenu } from "../../../../../../components/settings/ApiKeys/Menu";
|
||||
|
||||
export function AnalyticsPageAPIKeysMenu(props: {
|
||||
apiKeys: Pick<ApiKey, "name" | "key">[];
|
||||
selectedAPIKey: Pick<ApiKey, "name" | "key">;
|
||||
}) {
|
||||
const router = useDashboardRouter();
|
||||
return (
|
||||
<ApiKeysMenu
|
||||
apiKeys={props.apiKeys}
|
||||
selectedKey={props.selectedAPIKey}
|
||||
onSelect={(key) => {
|
||||
router.push(`/dashboard/connect/analytics/${key.key}`);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
import { ConnectSDKCard } from "../../../../../../components/shared/ConnectSDKCard";
|
||||
import { getApiKeys } from "../../../../../api/lib/getAPIKeys";
|
||||
import { getAuthToken } from "../../../../../api/lib/getAuthToken";
|
||||
import { ConnectAnalyticsDashboard } from "../../../../../team/[team_slug]/[project_slug]/connect/analytics/ConnectAnalyticsDashboard";
|
||||
import { AnalyticsPageAPIKeysMenu } from "./AnalyticsPageAPIKeysMenu";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{
|
||||
clientId: string;
|
||||
}>;
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const { clientId } = await props.params;
|
||||
|
||||
if (!authToken) {
|
||||
redirect(
|
||||
`/login?next=${encodeURIComponent(`/dashboard/connect/analytics/${clientId}`)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const _apiKeys = await getApiKeys();
|
||||
// slimmed down version to reduce client side payload
|
||||
const apiKeys = _apiKeys.map((key) => ({
|
||||
name: key.name,
|
||||
key: key.key,
|
||||
}));
|
||||
|
||||
const apiKey = apiKeys.find((key) => key.key === clientId);
|
||||
|
||||
if (!apiKey) {
|
||||
redirect("/dashboard/connect/analytics");
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h1 className="mb-1 font-semibold text-3xl tracking-tight">
|
||||
Connect Analytics
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Visualize how users are connecting to your apps.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<AnalyticsPageAPIKeysMenu apiKeys={apiKeys} selectedAPIKey={apiKey} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-4 lg:h-8" />
|
||||
|
||||
<ConnectAnalyticsDashboard
|
||||
clientId={apiKey.key}
|
||||
connectLayoutSlug="/dashboard/connect"
|
||||
/>
|
||||
|
||||
<div className="h-4 lg:h-8" />
|
||||
<ConnectSDKCard description="Add the Connect SDK to your app to start collecting analytics." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { redirect } from "next/navigation";
|
||||
import { ConnectSDKCard } from "../../../../../components/shared/ConnectSDKCard";
|
||||
import { getApiKeys } from "../../../../api/lib/getAPIKeys";
|
||||
import { getAuthToken } from "../../../../api/lib/getAuthToken";
|
||||
|
||||
// base page
|
||||
// - if no keys -> show no keys page
|
||||
// - else redirect to first key
|
||||
|
||||
export default async function Page() {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
redirect(
|
||||
`/login?next=${encodeURIComponent("/dashboard/connect/analytics")}`,
|
||||
);
|
||||
}
|
||||
|
||||
const apiKeys = await getApiKeys();
|
||||
const firstKey = apiKeys[0];
|
||||
|
||||
if (firstKey) {
|
||||
redirect(`/dashboard/connect/analytics/${firstKey.key}`);
|
||||
}
|
||||
|
||||
return <NoKeysCreatedPage />;
|
||||
}
|
||||
|
||||
function NoKeysCreatedPage() {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div>
|
||||
<h1 className="mb-1 font-semibold text-3xl tracking-tight">
|
||||
Connect Analytics
|
||||
</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Visualize how users are connecting to your apps.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-4 lg:h-8" />
|
||||
|
||||
<div className="flex flex-col items-center rounded-lg border border-border bg-muted/50 px-4 py-10 lg:px-6">
|
||||
<h3 className="mb-3 font-semibold text-2xl">No API keys found</h3>
|
||||
<p className="mb-6 text-muted-foreground text-sm">
|
||||
Start using the Connect SDK in your app with a free API key.
|
||||
</p>
|
||||
<Button asChild variant="primary">
|
||||
<Link href="/dashboard/settings/api-keys">Create API Key</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="h-4 lg:h-8" />
|
||||
<ConnectSDKCard description="Add the Connect SDK to your app to start collecting analytics." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
import { EcosystemLayoutSlug } from "./components/EcosystemSlugLayout";
|
||||
|
||||
export default async function Layout(props: {
|
||||
children: React.ReactNode;
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const params = await props.params;
|
||||
|
||||
const { children } = props;
|
||||
|
||||
return (
|
||||
<EcosystemLayoutSlug
|
||||
params={params}
|
||||
ecosystemLayoutPath="/dashboard/connect/ecosystem"
|
||||
>
|
||||
{children}
|
||||
</EcosystemLayoutSlug>
|
||||
);
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -1,7 +0,0 @@
|
||||
import { EcosystemCreatePage } from "./EcosystemCreatePage";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<EcosystemCreatePage ecosystemLayoutPath="/dashboard/connect/ecosystem" />
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
@@ -1,7 +0,0 @@
|
||||
import { EcosystemLandingPage } from "./EcosystemLandingPage";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<EcosystemLandingPage ecosystemLayoutPath="/dashboard/connect/ecosystem" />
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { TrackedLinkTW } from "@/components/ui/tracked-link";
|
||||
|
||||
export function PageHeader() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="font-semibold text-3xl tracking-tigher">In-App Wallets</h1>
|
||||
<div className="h-3" />
|
||||
<p className="max-w-[500px] text-muted-foreground ">
|
||||
A wallet infrastructure that enables apps to create, manage, and control
|
||||
their users wallets. Email login, social login, and bring-your-own auth
|
||||
supported.{" "}
|
||||
<TrackedLinkTW
|
||||
target="_blank"
|
||||
href="https://portal.thirdweb.com/connect/in-app-wallet/overview"
|
||||
label="learn-more"
|
||||
category="embedded-wallet"
|
||||
className="text-link-foreground hover:text-foreground"
|
||||
>
|
||||
Learn more
|
||||
</TrackedLinkTW>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
-32
@@ -1,32 +0,0 @@
|
||||
"use client";
|
||||
import { TabLinks } from "@/components/ui/tabs";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
export function Tabs({ clientId }: { clientId: string }) {
|
||||
const path = usePathname();
|
||||
|
||||
return (
|
||||
<TabLinks
|
||||
links={[
|
||||
{
|
||||
name: "Analytics",
|
||||
href: `/dashboard/connect/in-app-wallets/${clientId}/analytics`,
|
||||
isActive: path?.endsWith("/analytics") || false,
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
name: "Users",
|
||||
href: `/dashboard/connect/in-app-wallets/${clientId}/users`,
|
||||
isActive: path?.endsWith("/users") || false,
|
||||
isDisabled: false,
|
||||
},
|
||||
{
|
||||
name: "Configuration",
|
||||
href: `/dashboard/connect/in-app-wallets/${clientId}/config`,
|
||||
isActive: path?.endsWith("/config") || false,
|
||||
isDisabled: false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-1
@@ -1 +0,0 @@
|
||||
export const TRACKING_CATEGORY = "embedded-wallet";
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
import type { Range } from "components/analytics/date-range-selector";
|
||||
import { InAppWalletAnalytics } from "components/embedded-wallets/Analytics";
|
||||
|
||||
export default async function Page(props: {
|
||||
params: Promise<{ clientId: string }>;
|
||||
searchParams: Promise<{
|
||||
from?: string;
|
||||
to?: string;
|
||||
type?: string;
|
||||
interval?: string;
|
||||
}>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
const params = await props.params;
|
||||
const range =
|
||||
searchParams.from && searchParams.to
|
||||
? {
|
||||
type: searchParams.type ?? "last-120",
|
||||
from: new Date(searchParams.from),
|
||||
to: new Date(searchParams.to),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const interval: "day" | "week" = ["day", "week"].includes(
|
||||
searchParams.interval ?? "",
|
||||
)
|
||||
? (searchParams.interval as "day" | "week")
|
||||
: "week";
|
||||
|
||||
return (
|
||||
<InAppWalletAnalytics
|
||||
clientId={params.clientId}
|
||||
interval={interval}
|
||||
range={range as Range}
|
||||
/>
|
||||
);
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
|
||||
export { GenericLoadingPage as default } from "@/components/blocks/skeletons/GenericLoadingPage";
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user