diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 09cff73b162..e678b68e75b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -63,7 +63,7 @@ jobs: - name: Setup Biome uses: biomejs/setup-biome@a9763ed3d2388f5746f9dc3e1a55df7f4609bc89 # v2.5.1 with: - version: latest + version: 2.0.0 - run: pnpm lint diff --git a/.vscode/settings.json b/.vscode/settings.json index 266684b034c..367183517b3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,25 +1,8 @@ { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit", - "source.organizeImports.biome": "explicit", - "quickfix.biome": "explicit" - }, - "editor.defaultFormatter": "biomejs.biome", - "typescript.preferences.autoImportFileExcludePatterns": [ - "./packages/thirdweb/src/exports" - ], - "typescript.preferences.autoImportSpecifierExcludeRegexes": [ - "@radix-ui", - "next/router", - "next/dist", - "^lucide-react/dist/lucide-react.suffixed$" - ], - "typescript.tsdk": "node_modules/typescript/lib", - "[typescriptreact]": { + "[css]": { "editor.defaultFormatter": "biomejs.biome" }, - "[typescript]": { + "[javascript]": { "editor.defaultFormatter": "biomejs.biome" }, "[json]": { @@ -28,14 +11,30 @@ "[markdown]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "[javascript]": { + "[typescript]": { "editor.defaultFormatter": "biomejs.biome" }, - "[css]": { + "[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" }, + "editor.codeActionsOnSave": { + "quickfix.biome": "explicit", + "source.fixAll.biome": "explicit" + }, + "editor.defaultFormatter": "biomejs.biome", + "editor.formatOnSave": true, "eslint.workingDirectories": [ { "pattern": "./packages/*/" }, { "pattern": "./apps/*/" } - ] + ], + "typescript.preferences.autoImportFileExcludePatterns": [ + "./packages/thirdweb/src/exports" + ], + "typescript.preferences.autoImportSpecifierExcludeRegexes": [ + "@radix-ui", + "next/router", + "next/dist", + "^lucide-react/dist/lucide-react.suffixed$" + ], + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/apps/dashboard/.eslintrc.js b/apps/dashboard/.eslintrc.js index 3c12095ad34..3b380fde313 100644 --- a/apps/dashboard/.eslintrc.js +++ b/apps/dashboard/.eslintrc.js @@ -1,51 +1,97 @@ module.exports = { + env: { + browser: true, + node: true, + }, extends: [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@next/next/recommended", "plugin:storybook/recommended", ], - rules: { - "react-compiler/react-compiler": "error", - "no-restricted-syntax": [ - "error", - { - selector: "CallExpression[callee.name='useEffect']", - message: - 'Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery".', + overrides: [ + // disable restricted imports in tw-components + { + files: "src/tw-components/**/*", + rules: { + "no-restricted-imports": ["off"], }, - { - selector: "CallExpression[callee.name='createContext']", - message: - 'Are you *sure* you need to use a "Context"? In almost all cases you should prefer passing props directly.', + }, + // allow direct PostHog imports inside analytics helpers + { + files: "src/@/analytics/**/*", + rules: { + "no-restricted-imports": ["off"], }, - { - selector: "CallExpression[callee.name='defineChain']", - message: - "Use useV5DashboardChain instead if you are using it inside a component", + }, + // enable rule specifically for TypeScript files + { + files: ["*.ts", "*.tsx"], + rules: { + "@typescript-eslint/explicit-module-boundary-types": ["off"], }, - { - selector: "CallExpression[callee.name='defineDashboardChain']", - message: - "Use useV5DashboardChain instead if you are using it inside a component", + }, + + // in test files, allow null assertions and anys and eslint is sometimes weird about the react-scope thing + { + files: ["*test.ts?(x)"], + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-non-null-assertion": "off", + + "react/display-name": "off", }, - { - selector: "CallExpression[callee.name='mapV4ChainToV5Chain']", - message: - "Use useV5DashboardChain instead if you are using it inside a component", + }, + // allow requires in non-transpiled JS files and logical key ordering in config files + { + files: [ + "babel-node.js", + "*babel.config.js", + "env.config.js", + "next.config.js", + "webpack.config.js", + "packages/mobile-web/package-builder/**", + ], + rules: {}, + }, + + // setupTests can have separated imports for logical grouping + { + files: ["setupTests.ts"], + rules: { + "import/newline-after-import": "off", }, - { - selector: "CallExpression[callee.name='resolveScheme']", - message: - "resolveScheme can throw error if resolution fails. Either catch the error and ignore the lint warning or Use `resolveSchemeWithErrorHandler` / `replaceIpfsUrl` utility in dashboard instead", + }, + // turn OFF unused vars via eslint + { + files: ["*.ts", "*.tsx"], + rules: { + "@next/next/no-img-element": "off", + "@typescript-eslint/no-unused-vars": "off", }, - ], + }, + // THIS NEEDS TO GO LAST! + { + extends: ["biome"], + files: ["*.ts", "*.js", "*.tsx", "*.jsx"], + }, + ], + parser: "@typescript-eslint/parser", + parserOptions: { + ecmaFeatures: { + impliedStrict: true, + jsx: true, + }, + ecmaVersion: 2019, + warnOnUnsupportedTypeScriptVersion: true, + }, + plugins: ["@typescript-eslint", "react-compiler"], + rules: { "no-restricted-imports": [ "error", { paths: [ { - name: "@chakra-ui/react", // these are provided by tw-components, so we don't want to import them from chakra directly importNames: [ "Card", @@ -82,52 +128,77 @@ module.exports = { ], message: 'Use the equivalent component from "tw-components" instead.', + name: "@chakra-ui/react", }, { - name: "@chakra-ui/layout", message: "Import from `@chakra-ui/react` instead of `@chakra-ui/layout`.", + name: "@chakra-ui/layout", }, { - name: "@chakra-ui/button", message: "Import from `@chakra-ui/react` instead of `@chakra-ui/button`.", + name: "@chakra-ui/button", }, { - name: "@chakra-ui/menu", message: "Import from `@chakra-ui/react` instead of `@chakra-ui/menu`.", + name: "@chakra-ui/menu", }, { - name: "next/navigation", importNames: ["useRouter"], message: 'Use `import { useDashboardRouter } from "@/lib/DashboardRouter";` instead', + name: "next/navigation", }, { - name: "lucide-react", importNames: ["Link", "Table", "Sidebar"], message: 'This is likely a mistake. If you really want to import this - postfix the imported name with Icon. Example - "LinkIcon"', + name: "lucide-react", }, { - name: "posthog-js", message: 'Import "posthog-js" directly only within the analytics helpers ("src/@/analytics/*"). Use the exported helpers from "@/analytics/track" elsewhere.', + name: "posthog-js", }, ], }, ], - }, - parser: "@typescript-eslint/parser", - plugins: ["@typescript-eslint", "react-compiler"], - parserOptions: { - ecmaVersion: 2019, - ecmaFeatures: { - impliedStrict: true, - jsx: true, - }, - warnOnUnsupportedTypeScriptVersion: true, + "no-restricted-syntax": [ + "error", + { + message: + 'Are you *sure* you need to use "useEffect" here? If you loading any async function prefer using "useQuery".', + selector: "CallExpression[callee.name='useEffect']", + }, + { + message: + 'Are you *sure* you need to use a "Context"? In almost all cases you should prefer passing props directly.', + selector: "CallExpression[callee.name='createContext']", + }, + { + message: + "Use useV5DashboardChain instead if you are using it inside a component", + selector: "CallExpression[callee.name='defineChain']", + }, + { + message: + "Use useV5DashboardChain instead if you are using it inside a component", + selector: "CallExpression[callee.name='defineDashboardChain']", + }, + { + message: + "Use useV5DashboardChain instead if you are using it inside a component", + selector: "CallExpression[callee.name='mapV4ChainToV5Chain']", + }, + { + message: + "resolveScheme can throw error if resolution fails. Either catch the error and ignore the lint warning or Use `resolveSchemeWithErrorHandler` / `replaceIpfsUrl` utility in dashboard instead", + selector: "CallExpression[callee.name='resolveScheme']", + }, + ], + "react-compiler/react-compiler": "error", }, settings: { react: { @@ -136,67 +207,4 @@ module.exports = { version: "detect", }, }, - overrides: [ - // disable restricted imports in tw-components - { - files: "src/tw-components/**/*", - rules: { - "no-restricted-imports": ["off"], - }, - }, - // allow direct PostHog imports inside analytics helpers - { - files: "src/@/analytics/**/*", - rules: { - "no-restricted-imports": ["off"], - }, - }, - // enable rule specifically for TypeScript files - { - files: ["*.ts", "*.tsx"], - rules: { - "@typescript-eslint/explicit-module-boundary-types": ["off"], - }, - }, - - // in test files, allow null assertions and anys and eslint is sometimes weird about the react-scope thing - { - files: ["*test.ts?(x)"], - rules: { - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-explicit-any": "off", - - "react/display-name": "off", - }, - }, - // allow requires in non-transpiled JS files and logical key ordering in config files - { - files: [ - "babel-node.js", - "*babel.config.js", - "env.config.js", - "next.config.js", - "webpack.config.js", - "packages/mobile-web/package-builder/**", - ], - rules: {}, - }, - - // setupTests can have separated imports for logical grouping - { - files: ["setupTests.ts"], - rules: { - "import/newline-after-import": "off", - }, - }, - // THIS NEEDS TO GO LAST! - { - files: ["*.ts", "*.js", "*.tsx", "*.jsx"], - extends: ["biome"], - }, - ], - env: { - browser: true, - node: true, - }, }; diff --git a/apps/dashboard/biome.json b/apps/dashboard/biome.json index b287b3aca79..780bbdcd1af 100644 --- a/apps/dashboard/biome.json +++ b/apps/dashboard/biome.json @@ -1,16 +1,4 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.2/schema.json", - "extends": ["../../biome.json"], - "overrides": [ - { - "include": ["src/css/swagger-ui.css"], - "linter": { - "rules": { - "suspicious": { - "noImportantInKeyframe": "off" - } - } - } - } - ] + "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json", + "extends": "//" } diff --git a/apps/dashboard/checkly.config.ts b/apps/dashboard/checkly.config.ts index 837a6db2a21..756b299fb90 100644 --- a/apps/dashboard/checkly.config.ts +++ b/apps/dashboard/checkly.config.ts @@ -2,29 +2,29 @@ import { defineConfig } from "checkly"; import { Frequency } from "checkly/constructs"; export default defineConfig({ - projectName: "thirdweb.com", - logicalId: "thirdweb-www", - repoUrl: "https://github.com/thirdweb-dev/dashboard", checks: { activated: true, - muted: false, - runtimeId: "2023.09", - frequency: Frequency.EVERY_24H, - locations: ["us-east-1", "eu-west-1"], - tags: ["website"], + browserChecks: { + frequency: Frequency.EVERY_24H, + testMatch: "./tests/**/*.spec.ts", + }, checkMatch: "./**/*.check.ts", + frequency: Frequency.EVERY_24H, ignoreDirectoriesMatch: [], + locations: ["us-east-1", "eu-west-1"], + muted: false, playwrightConfig: { use: { baseURL: process.env.ENVIRONMENT_URL || "https://thirdweb.com", }, }, - browserChecks: { - frequency: Frequency.EVERY_24H, - testMatch: "./tests/**/*.spec.ts", - }, + runtimeId: "2023.09", + tags: ["website"], }, cli: { runLocation: "eu-west-1", }, + logicalId: "thirdweb-www", + projectName: "thirdweb.com", + repoUrl: "https://github.com/thirdweb-dev/dashboard", }); diff --git a/apps/dashboard/components.json b/apps/dashboard/components.json index b1dc07eb643..d6f9c61bbe9 100644 --- a/apps/dashboard/components.json +++ b/apps/dashboard/components.json @@ -1,17 +1,17 @@ { "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + }, "rsc": true, - "tsx": true, + "style": "default", "tailwind": { + "baseColor": "neutral", "config": "tailwind.config.js", "css": "src/global.css", - "baseColor": "neutral", "cssVariables": true, "prefix": "" }, - "aliases": { - "components": "@/components", - "utils": "@/lib/utils" - } -} \ No newline at end of file + "tsx": true +} diff --git a/apps/dashboard/knip.json b/apps/dashboard/knip.json index 4e487ef46d3..a063b5a4a8f 100644 --- a/apps/dashboard/knip.json +++ b/apps/dashboard/knip.json @@ -1,18 +1,18 @@ { "$schema": "https://unpkg.com/knip@5/schema.json", - "next": true, "ignore": [ "src/@/components/ui/**", "src/components/notices/AnnouncementBanner.tsx", "src/components/cmd-k-search/index.tsx", "src/lib/search.ts" ], - "project": ["src/**"], - "ignoreBinaries": ["only-allow", "biome"], + "ignoreBinaries": ["only-allow"], "ignoreDependencies": [ "@thirdweb-dev/service-utils", "@thirdweb-dev/vault-sdk", "@types/color", "fast-xml-parser" - ] + ], + "next": true, + "project": ["src/**"] } diff --git a/apps/dashboard/next-sitemap.config.js b/apps/dashboard/next-sitemap.config.js index 13c4a59ed2e..1f69b0162dc 100644 --- a/apps/dashboard/next-sitemap.config.js +++ b/apps/dashboard/next-sitemap.config.js @@ -44,12 +44,37 @@ async function getSingleChain(chainIdOrSlug) { /** @type {import('next-sitemap').IConfig} */ module.exports = { - siteUrl: process.env.SITE_URL || "https://thirdweb.com", + additionalPaths: async (config) => { + const [framerUrls, allChains] = await Promise.all([ + getFramerXML(), + fetchChainsFromApi(), + ]); + + return [ + ...framerUrls.map((url) => { + return { + changefreq: config.changefreq, + lastmod: config.autoLastmod ? new Date().toISOString() : undefined, + loc: url.loc, + priority: config.priority, + }; + }), + ...allChains.map((chain) => { + return { + changefreq: config.changefreq, + lastmod: config.autoLastmod ? new Date().toISOString() : undefined, + loc: `/${chain.slug}`, + priority: config.priority, + }; + }), + ...(await createSearchRecordSitemaps(config)), + ]; + }, + exclude: ["/chain/validate"], generateRobotsTxt: true, robotsTxtOptions: { policies: [ { - userAgent: "*", // allow all if production allow: process.env.VERCEL_ENV === "production" ? ["/"] : [], // disallow all if not production @@ -58,10 +83,11 @@ module.exports = { ? ["/"] : // disallow `/team` and `/team/*` if production ["/team", "/team/*"], + userAgent: "*", }, ], }, - exclude: ["/chain/validate"], + siteUrl: process.env.SITE_URL || "https://thirdweb.com", transform: async (config, _path) => { let path = _path; @@ -75,40 +101,14 @@ module.exports = { path = path.replace("deployer.thirdweb.eth", "thirdweb.eth"); } return { + alternateRefs: config.alternateRefs ?? [], + changefreq: config.changefreq, + lastmod: config.autoLastmod ? new Date().toISOString() : undefined, // => this will be exported as http(s):/// loc: path, - changefreq: config.changefreq, priority: config.priority, - lastmod: config.autoLastmod ? new Date().toISOString() : undefined, - alternateRefs: config.alternateRefs ?? [], }; }, - additionalPaths: async (config) => { - const [framerUrls, allChains] = await Promise.all([ - getFramerXML(), - fetchChainsFromApi(), - ]); - - return [ - ...framerUrls.map((url) => { - return { - loc: url.loc, - changefreq: config.changefreq, - priority: config.priority, - lastmod: config.autoLastmod ? new Date().toISOString() : undefined, - }; - }), - ...allChains.map((chain) => { - return { - loc: `/${chain.slug}`, - changefreq: config.changefreq, - priority: config.priority, - lastmod: config.autoLastmod ? new Date().toISOString() : undefined, - }; - }), - ...(await createSearchRecordSitemaps(config)), - ]; - }, }; /** * @param {{ changefreq?: any; priority?: any; autoLastmod?: any; }} config @@ -131,10 +131,10 @@ async function createSearchRecordSitemaps(config) { parsedLines.map((parsedLine) => { return getSingleChain(parsedLine.chain_id) .then((parsedLineChain) => ({ - loc: `/${parsedLineChain.slug}/${parsedLine.contract_address}`, changefreq: config.changefreq, - priority: config.priority, lastmod: config.autoLastmod ? new Date().toISOString() : undefined, + loc: `/${parsedLineChain.slug}/${parsedLine.contract_address}`, + priority: config.priority, })) .catch(() => null); }), diff --git a/apps/dashboard/next.config.ts b/apps/dashboard/next.config.ts index cdf880e3d9f..4baa4b86582 100644 --- a/apps/dashboard/next.config.ts +++ b/apps/dashboard/next.config.ts @@ -46,91 +46,90 @@ function determineIpfsGateways() { const remotePatterns: RemotePattern[] = []; if (process.env.API_ROUTES_CLIENT_ID) { remotePatterns.push({ - protocol: "https", hostname: `${process.env.API_ROUTES_CLIENT_ID}.ipfscdn.io`, + protocol: "https", }); remotePatterns.push({ - protocol: "https", hostname: `${process.env.API_ROUTES_CLIENT_ID}.thirdwebstorage-staging.com`, + protocol: "https", }); remotePatterns.push({ - protocol: "https", hostname: `${process.env.API_ROUTES_CLIENT_ID}.thirdwebstorage-dev.com`, + protocol: "https", }); } else { // this should only happen in development remotePatterns.push({ - protocol: "https", hostname: "ipfs.io", + protocol: "https", }); } // also add the dashboard clientId ipfs gateway if it is set if (process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID) { remotePatterns.push({ - protocol: "https", hostname: `${process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID}.ipfscdn.io`, + protocol: "https", }); remotePatterns.push({ - protocol: "https", hostname: `${process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID}.thirdwebstorage-staging.com`, + protocol: "https", }); remotePatterns.push({ - protocol: "https", hostname: `${process.env.NEXT_PUBLIC_DASHBOARD_CLIENT_ID}.thirdwebstorage-dev.com`, + protocol: "https", }); } return remotePatterns; } const SENTRY_OPTIONS: SentryBuildOptions = { + // An auth token is required for uploading source maps. + authToken: process.env.SENTRY_AUTH_TOKEN, + + // 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, + + // Automatically tree-shake Sentry logger statements to reduce bundle size + disableLogger: true, // 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, + + // Routes browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers (increases server load) + tunnelRoute: "/err", // 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", - - // 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, }; // add additional languages to the framer rewrite paths here (english is already included by default) const FRAMER_ADDITIONAL_LANGUAGES = ["es"]; const baseNextConfig: NextConfig = { + compiler: { + emotion: true, + }, eslint: { ignoreDuringBuilds: true, }, - productionBrowserSourceMaps: false, experimental: { - webpackBuildWorker: true, - webpackMemoryOptimizations: true, serverSourceMaps: false, taint: true, + webpackBuildWorker: true, + webpackMemoryOptimizations: true, }, - serverExternalPackages: ["pino-pretty"], async headers() { return [ { - // Apply these headers to all routes in your application. - source: "/(.*)", headers: [ ...securityHeaders, { @@ -138,68 +137,69 @@ const baseNextConfig: NextConfig = { value: "sec-ch-viewport-width", }, ], + // Apply these headers to all routes in your application. + source: "/(.*)", }, ]; }, + images: { + contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", + dangerouslyAllowSVG: true, + remotePatterns: [ + { + hostname: "**.thirdweb.com", + protocol: "https", + }, + ...determineIpfsGateways(), + ], + }, + productionBrowserSourceMaps: false, + reactStrictMode: true, async redirects() { return getRedirects(); }, async rewrites() { return [ { - source: "/_ph/static/:path*", destination: "https://us-assets.i.posthog.com/static/:path*", + source: "/_ph/static/:path*", }, { - source: "/_ph/:path*", destination: "https://us.i.posthog.com/:path*", + source: "/_ph/:path*", }, { - source: "/_ph/decide", destination: "https://us.i.posthog.com/decide", + source: "/_ph/decide", }, { - source: "/thirdweb.eth", destination: "/deployer.thirdweb.eth", + source: "/thirdweb.eth", }, { - source: "/thirdweb.eth/:path*", destination: "/deployer.thirdweb.eth/:path*", + source: "/thirdweb.eth/:path*", }, // re-write /home to / (this is so that logged in users will be able to go to /home and NOT be redirected to the logged in app) { - source: "/home", destination: "https://landing.thirdweb.com", + source: "/home", }, // flatmap the framer paths for the default language and the additional languages ...FRAMER_PATHS.flatMap((path) => [ { - source: path, destination: `https://landing.thirdweb.com${path}`, + source: path, }, // this is for additional languages ...FRAMER_ADDITIONAL_LANGUAGES.map((lang) => ({ - source: `/${lang}${path}`, destination: `https://landing.thirdweb.com/${lang}${path}`, + source: `/${lang}${path}`, })), ]), ]; }, - images: { - dangerouslyAllowSVG: true, - contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;", - remotePatterns: [ - { - protocol: "https", - hostname: "**.thirdweb.com", - }, - ...determineIpfsGateways(), - ], - }, - compiler: { - emotion: true, - }, - reactStrictMode: true, + serverExternalPackages: ["pino-pretty"], }; function getConfig(): NextConfig { diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index d653afc294c..783f5d4b54a 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -1,26 +1,4 @@ { - "name": "thirdweb-dashboard", - "version": "3.0.0", - "private": true, - "scripts": { - "preinstall": "npx only-allow pnpm", - "dev": "next dev --turbopack", - "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", - "fix": "biome check ./src --fix && eslint ./src --fix", - "typecheck": "tsc --noEmit", - "gen:theme-typings": "chakra-cli tokens src/theme/index.ts", - "postinstall": "pnpm run gen:theme-typings", - "postbuild": "next-sitemap", - "build:analyze": "ANALYZE=true pnpm run build", - "knip": "knip", - "playwright": "playwright test", - "update-checkly": "npx checkly deploy", - "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build" - }, "dependencies": { "@chakra-ui/react": "^2.8.2", "@chakra-ui/styled-system": "^2.9.2", @@ -106,6 +84,7 @@ "zod": "3.25.62" }, "devDependencies": { + "@biomejs/biome": "2.0.0", "@chakra-ui/cli": "^2.4.1", "@chromatic-com/storybook": "4.0.0", "@next/bundle-analyzer": "15.3.3", @@ -139,5 +118,27 @@ "storybook": "9.0.8", "tailwindcss": "3.4.17", "typescript": "5.8.3" - } + }, + "name": "thirdweb-dashboard", + "private": true, + "scripts": { + "build": "NODE_OPTIONS=--max-old-space-size=6144 next build", + "build-storybook": "storybook build", + "build:analyze": "ANALYZE=true pnpm run build", + "dev": "next dev --turbopack", + "fix": "biome check ./src --fix && eslint ./src --fix", + "format": "biome format ./src --write", + "gen:theme-typings": "chakra-cli tokens src/theme/index.ts", + "knip": "knip", + "lint": "biome check ./src && knip && eslint ./src", + "playwright": "playwright test", + "postbuild": "next-sitemap", + "postinstall": "pnpm run gen:theme-typings", + "preinstall": "npx only-allow pnpm", + "start": "next start", + "storybook": "storybook dev -p 6006", + "typecheck": "tsc --noEmit", + "update-checkly": "npx checkly deploy" + }, + "version": "3.0.0" } diff --git a/apps/dashboard/playwright.config.ts b/apps/dashboard/playwright.config.ts index ca9c4b4b6ee..216151645d1 100644 --- a/apps/dashboard/playwright.config.ts +++ b/apps/dashboard/playwright.config.ts @@ -10,25 +10,10 @@ import { defineConfig, devices } from "@playwright/test"; * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: "./tests", - /* Run tests in files in parallel */ - fullyParallel: true, /* Fail the build on CI if you accidentally left test.only in the source code. */ forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: "html", - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: process.env.ENVIRONMENT_URL || "http://127.0.0.1:3000", - - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: "on-first-retry", - }, + /* Run tests in files in parallel */ + fullyParallel: true, /* Configure projects for major browsers */ projects: [ @@ -67,13 +52,28 @@ export default defineConfig({ // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, // }, ], + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + testDir: "./tests", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: process.env.ENVIRONMENT_URL || "http://127.0.0.1:3000", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, /* Run your local dev server before starting the tests */ webServer: process.env.CI ? undefined : { command: "pnpm run start", - url: "http://127.0.0.1:3000", reuseExistingServer: !process.env.CI, + url: "http://127.0.0.1:3000", }, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, }); diff --git a/apps/dashboard/postcss.config.js b/apps/dashboard/postcss.config.js index 12a703d900d..4df9d3ba36d 100644 --- a/apps/dashboard/postcss.config.js +++ b/apps/dashboard/postcss.config.js @@ -1,6 +1,6 @@ module.exports = { plugins: { - tailwindcss: {}, autoprefixer: {}, + tailwindcss: {}, }, }; diff --git a/apps/dashboard/redirects.js b/apps/dashboard/redirects.js index 837fa6a5c5c..39517d59761 100644 --- a/apps/dashboard/redirects.js +++ b/apps/dashboard/redirects.js @@ -1,64 +1,64 @@ const legacyDashboardToTeamRedirects = [ { - source: "/dashboard", destination: "/team", permanent: false, + source: "/dashboard", }, { - source: "/dashboard/contracts/:path*", destination: "/team/~/~/contracts", permanent: false, + source: "/dashboard/contracts/:path*", }, { - source: "/dashboard/connect/ecosystem/:path*", destination: "/team/~/~/ecosystem/:path*", permanent: false, + source: "/dashboard/connect/ecosystem/:path*", }, { - source: "/dashboard/engine/:path*", destination: "/team/~/~/engine/:path*", permanent: false, + source: "/dashboard/engine/:path*", }, { - source: "/dashboard/settings/api-keys", destination: "/team", permanent: false, + source: "/dashboard/settings/api-keys", }, { - source: "/dashboard/settings/devices", destination: "/account/devices", permanent: false, + source: "/dashboard/settings/devices", }, { - source: "/dashboard/settings/billing", destination: "/team/~/~/settings/billing", permanent: false, + source: "/dashboard/settings/billing", }, { - source: "/dashboard/settings/gas-credits", destination: "/team/~/~/settings/credits", permanent: false, + source: "/dashboard/settings/gas-credits", }, { - source: "/dashboard/settings/usage", destination: "/team/~/~/usage", permanent: false, + source: "/dashboard/settings/usage", }, { - source: "/dashboard/settings/storage", destination: "/team/~/~/usage/storage", permanent: false, + source: "/dashboard/settings/storage", }, { - source: "/dashboard/settings/notifications", destination: "/team/~/~/settings/notifications", permanent: false, + source: "/dashboard/settings/notifications", }, // rest of the /dashboard/* routes { - source: "/dashboard/:path*", destination: "/team", permanent: false, + source: "/dashboard/:path*", }, ]; @@ -66,321 +66,321 @@ const legacyDashboardToTeamRedirects = [ async function redirects() { return [ { - source: "/portal/:match*", destination: "https://portal.thirdweb.com/:match*", permanent: true, + source: "/portal/:match*", }, { - source: "/solutions/appchain-api", destination: "/solutions/chains", permanent: true, + source: "/solutions/appchain-api", }, { - source: "/contracts/release", destination: "/contracts/publish", permanent: false, + source: "/contracts/release", }, { - source: "/contracts/release/:path*", destination: "/contracts/publish/:path*", permanent: false, + source: "/contracts/release/:path*", }, { - source: "/release", destination: "/publish", permanent: false, + source: "/release", }, { - source: "/release/:path*", destination: "/publish/:path*", permanent: false, + source: "/release/:path*", }, { - source: "/authentication", destination: "/auth", permanent: false, + source: "/authentication", }, { - source: "/extensions", destination: "/build", permanent: false, + source: "/extensions", }, { - source: "/contractkit", destination: "/build", permanent: true, + source: "/contractkit", }, // old (deprecated) routes { - source: - "/:network/(edition|nft-collection|token|nft-drop|signature-drop|edition-drop|token-drop|vote)/:address", destination: "/:network/:address", permanent: false, + source: + "/:network/(edition|nft-collection|token|nft-drop|signature-drop|edition-drop|token-drop|vote)/:address", }, // prebuilt contract deploys { - source: "/contracts/new/:slug*", destination: "/explore", permanent: false, + source: "/contracts/new/:slug*", }, // deployer to non-deployer url // handled directly in SSR as well { - source: "/deployer.thirdweb.eth", destination: "/thirdweb.eth", permanent: true, + source: "/deployer.thirdweb.eth", }, { - source: "/deployer.thirdweb.eth/:path*", destination: "/thirdweb.eth/:path*", permanent: true, + source: "/deployer.thirdweb.eth/:path*", }, { - source: "/chains", destination: "/chainlist", permanent: true, + source: "/chains", }, // polygon zkevm beta to non-beta { - source: "/polygon-zkevm-beta", destination: "/polygon-zkevm", permanent: false, + source: "/polygon-zkevm-beta", }, // backwards compat: page moved to pages/settings/devices { - source: "/template/nft-drop", destination: "/template/erc721", permanent: false, + source: "/template/nft-drop", }, { - source: "/create-api-key", destination: "/team", permanent: false, + source: "/create-api-key", }, { - source: "/dashboard/settings", destination: "/team", permanent: false, + source: "/dashboard/settings", }, { - source: "/dashboard/connect/playground", destination: "https://playground.thirdweb.com/connect/sign-in/button", permanent: false, + source: "/dashboard/connect/playground", }, { - source: "/dashboard/infrastructure/storage", destination: "/dashboard/settings/storage", permanent: false, + source: "/dashboard/infrastructure/storage", }, { - source: "/dashboard/infrastructure/rpc-edge", destination: "/chainlist", permanent: false, + source: "/dashboard/infrastructure/rpc-edge", }, { - source: "/solutions/commerce", destination: "/solutions/loyalty", permanent: false, + source: "/solutions/commerce", }, { - source: "/hackathon/base-consumer-crypto", destination: "/hackathon/consumer-crypto", permanent: false, + source: "/hackathon/base-consumer-crypto", }, { - source: "/bear-market-airdrop", destination: "/", permanent: false, + source: "/bear-market-airdrop", }, { - source: "/drops/optimism", destination: "/optimism", permanent: false, + source: "/drops/optimism", }, // Redirecting as ambassadors lives in community now { - source: "/ambassadors", destination: "/community/ambassadors", permanent: false, + source: "/ambassadors", }, { - source: "/embedded-wallets", destination: "/in-app-wallets", permanent: false, + source: "/embedded-wallets", }, // temporarily redirect cli login to support page { - source: "/cli/login", destination: "https://portal.thirdweb.com/knowledge-base/onchain-common-errors/thirdweb-cli/device-link-error", permanent: false, + source: "/cli/login", }, // temporary redirect gas -> explore page { - source: "/gas", destination: "/explore", permanent: false, + source: "/gas", }, { - source: "/deploy", destination: "/contracts/deployment-tool", permanent: false, + source: "/deploy", }, { - source: "/publish", destination: "/contracts/deployment-tool", permanent: false, + source: "/publish", }, { - source: "/smart-contracts", destination: "/contracts/explore", permanent: false, + source: "/smart-contracts", }, { - source: "/ui-components", destination: "/sdk", permanent: false, + source: "/ui-components", }, { - source: "/interact", destination: "/sdk", permanent: false, + source: "/interact", }, { - source: "/sponsored-transactions", destination: "/account-abstraction", permanent: false, + source: "/sponsored-transactions", }, // redirect /solutions/chains to /solutions/ecosystem { - source: "/solutions/chains", destination: "/solutions/ecosystem", permanent: false, + source: "/solutions/chains", }, // redirect /rpc to portal { - source: "/rpc-edge", destination: "https://portal.thirdweb.com/infrastructure/rpc-edge/overview", permanent: false, + source: "/rpc-edge", }, // redirect /sdk to portal { - source: "/sdk", destination: "https://portal.thirdweb.com/connect/blockchain-api", permanent: false, + source: "/sdk", }, // redirect `/events` to homepage { - source: "/events", destination: "/", permanent: false, + source: "/events", }, // redirect /community to /community/ambassadors { - source: "/community", destination: "/community/ambassadors", permanent: false, + source: "/community", }, // redirect `/tos` to `/terms` { - source: "/tos", destination: "/terms", permanent: false, + source: "/tos", }, // redirect `/privacy` to `/privacy-policy` { - source: "/privacy", destination: "/privacy-policy", permanent: false, + source: "/privacy", }, // redirect `/mission` to `/home` { - source: "/mission", destination: "/home", permanent: false, + source: "/mission", }, // redirect "/open-source" to "/bounties" { - source: "/open-source", destination: "/bounties", permanent: false, + source: "/open-source", }, // redirect /template/ to /templates/ { - source: "/template/:slug", destination: "/templates/:slug", permanent: false, + source: "/template/:slug", }, // redirect /connect/pay to /universal-bridge { - source: "/connect/pay", destination: "/universal-bridge", permanent: false, + source: "/connect/pay", }, // PREVIOUS CAMPAIGNS { - source: "/unlimited-wallets", destination: "/", permanent: false, + source: "/unlimited-wallets", }, // pay > universal-bridge redirect { - source: "/team/:team_slug/:project_slug/connect/pay/:path*", destination: "/team/:team_slug/:project_slug/connect/universal-bridge/:path*", permanent: false, + source: "/team/:team_slug/:project_slug/connect/pay/:path*", }, // all /learn/tutorials (and sub-routes) -> /learn/guides { - source: "/learn/tutorials/:path*", destination: "/learn/guides/:path*", permanent: false, + source: "/learn/tutorials/:path*", }, { - source: "/learn/tutorials", destination: "/learn/guides", permanent: false, + source: "/learn/tutorials", }, // redirect to /grant/superchain to /superchain { - source: "/grant/superchain", destination: "/superchain", permanent: false, + source: "/grant/superchain", }, // connect -> build redirects { - source: "/connect", destination: "/wallets", permanent: false, + source: "/connect", }, { - source: "/connect/account-abstraction", destination: "/account-abstraction", permanent: false, + source: "/connect/account-abstraction", }, { - source: "/connect/universal-bridge", destination: "/universal-bridge", permanent: false, + source: "/connect/universal-bridge", }, { - source: "/connect/auth", destination: "/auth", permanent: false, + source: "/connect/auth", }, { - source: "/connect/in-app-wallets", destination: "/in-app-wallets", permanent: false, + source: "/connect/in-app-wallets", }, { - source: "/engine", destination: "/transactions", permanent: false, + source: "/engine", }, ...legacyDashboardToTeamRedirects, diff --git a/apps/dashboard/sentry.client.config.ts b/apps/dashboard/sentry.client.config.ts index aa8b26ff8fa..b7ad8de6860 100644 --- a/apps/dashboard/sentry.client.config.ts +++ b/apps/dashboard/sentry.client.config.ts @@ -5,28 +5,32 @@ import * as Sentry from "@sentry/nextjs"; Sentry.init({ - dsn: "https://8813f5d93c8c4aa89eda86816f0b1bbf@o1378374.ingest.sentry.io/6690186", - - // Adjust this value in production, or use tracesSampler for greater control - tracesSampleRate: 0.1, + allowUrls: [/thirdweb-dev\.com/i, /thirdweb\.com/, /thirdweb-preview\.com/], // Setting this option to true will print useful information to the console while you're setting up Sentry. debug: false, - - replaysOnErrorSampleRate: 1.0, - - // This sets the sample rate to be 10%. You may want this to be 100% while - // in development and sample at a lower rate in production - replaysSessionSampleRate: 0.1, - - // You can remove this option if you're not planning to use the Sentry Session Replay feature: - integrations: [ - Sentry.replayIntegration({ - // Additional Replay configuration goes in here, for example: - maskAllText: true, - blockAllMedia: true, - }), + denyUrls: [ + // Google Adsense + /pagead\/js/i, + // Facebook flakiness + /graph\.facebook\.com/i, + // Facebook blocked + /connect\.facebook\.net\/en_US\/all\.js/i, + // Woopra flakiness + /eatdifferent\.com\.woopra-ns\.com/i, + /static\.woopra\.com\/js\/woopra\.js/i, + // Chrome extensions + /extensions\//i, + /^chrome:\/\//i, + // Other plugins + // Cacaoweb + /127\.0\.0\.1:4001\/isrunning/i, + /webappstoolbarba\.texthelp\.com\//i, + /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, + // injected (extensions) + /inject/i, ], + dsn: "https://8813f5d93c8c4aa89eda86816f0b1bbf@o1378374.ingest.sentry.io/6690186", ignoreErrors: [ // Random plugins/extensions "top.GLOBALS", @@ -72,26 +76,22 @@ Sentry.init({ // cannot do anything with these errors "Non-Error promise rejection captured", ], - denyUrls: [ - // Google Adsense - /pagead\/js/i, - // Facebook flakiness - /graph\.facebook\.com/i, - // Facebook blocked - /connect\.facebook\.net\/en_US\/all\.js/i, - // Woopra flakiness - /eatdifferent\.com\.woopra-ns\.com/i, - /static\.woopra\.com\/js\/woopra\.js/i, - // Chrome extensions - /extensions\//i, - /^chrome:\/\//i, - // Other plugins - // Cacaoweb - /127\.0\.0\.1:4001\/isrunning/i, - /webappstoolbarba\.texthelp\.com\//i, - /metrics\.itunes\.apple\.com\.edgesuite\.net\//i, - // injected (extensions) - /inject/i, + + // You can remove this option if you're not planning to use the Sentry Session Replay feature: + integrations: [ + Sentry.replayIntegration({ + blockAllMedia: true, + // Additional Replay configuration goes in here, for example: + maskAllText: true, + }), ], - allowUrls: [/thirdweb-dev\.com/i, /thirdweb\.com/, /thirdweb-preview\.com/], + + replaysOnErrorSampleRate: 1.0, + + // This sets the sample rate to be 10%. You may want this to be 100% while + // in development and sample at a lower rate in production + replaysSessionSampleRate: 0.1, + + // Adjust this value in production, or use tracesSampler for greater control + tracesSampleRate: 0.1, }); diff --git a/apps/dashboard/src/@/actions/acceptInvite.ts b/apps/dashboard/src/@/actions/acceptInvite.ts index 8a579732083..ee71e244632 100644 --- a/apps/dashboard/src/@/actions/acceptInvite.ts +++ b/apps/dashboard/src/@/actions/acceptInvite.ts @@ -11,20 +11,20 @@ export async function acceptInvite(options: { if (!token) { return { - ok: false, errorMessage: "You are not authorized to perform this action", + ok: false, }; } const res = await fetch( `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/invites/${options.inviteId}/accept`, { - method: "POST", + body: JSON.stringify({}), headers: { - "Content-Type": "application/json", Authorization: `Bearer ${token}`, + "Content-Type": "application/json", }, - body: JSON.stringify({}), + method: "POST", }, ); @@ -42,8 +42,8 @@ export async function acceptInvite(options: { } catch {} return { - ok: false, errorMessage, + ok: false, }; } diff --git a/apps/dashboard/src/@/actions/billing.ts b/apps/dashboard/src/@/actions/billing.ts index 5af78101fc4..51b9ab3d897 100644 --- a/apps/dashboard/src/@/actions/billing.ts +++ b/apps/dashboard/src/@/actions/billing.ts @@ -16,12 +16,12 @@ export async function reSubscribePlan(options: { const res = await fetch( `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/checkout/resubscribe-plan`, { - method: "PUT", + body: JSON.stringify({}), headers: { - "Content-Type": "application/json", Authorization: `Bearer ${token}`, + "Content-Type": "application/json", }, - body: JSON.stringify({}), + method: "PUT", }, ); diff --git a/apps/dashboard/src/@/actions/confirmEmail.ts b/apps/dashboard/src/@/actions/confirmEmail.ts index e9a47325158..db55c639d3e 100644 --- a/apps/dashboard/src/@/actions/confirmEmail.ts +++ b/apps/dashboard/src/@/actions/confirmEmail.ts @@ -15,14 +15,14 @@ export async function confirmEmailWithOTP(otp: string) { const res = await fetch( `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/account/confirmEmail`, { - method: "PUT", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token}`, - }, body: JSON.stringify({ confirmationToken: otp, }), + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + method: "PUT", }, ); diff --git a/apps/dashboard/src/@/actions/createTeam.ts b/apps/dashboard/src/@/actions/createTeam.ts index 53f726be8a6..417e702a141 100644 --- a/apps/dashboard/src/@/actions/createTeam.ts +++ b/apps/dashboard/src/@/actions/createTeam.ts @@ -3,62 +3,59 @@ import "server-only"; // biome-ignore lint/style/useNodejsImportProtocol: breaks storybook if it's `node:` prefixed import { randomBytes } from "crypto"; -import type { Team } from "@/api/team"; import { format } from "date-fns"; +import type { Team } from "@/api/team"; import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken"; import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs"; -export async function createTeam(options?: { - name?: string; - slug?: string; -}) { +export async function createTeam(options?: { name?: string; slug?: string }) { const token = await getAuthToken(); if (!token) { return { - status: "error", errorMessage: "You are not authorized to perform this action", + status: "error", } as const; } const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, body: JSON.stringify({ + billingEmail: null, + image: null, name: options?.name ?? `Your Projects ${format(new Date(), "MMM d yyyy")}`, slug: options?.slug ?? randomBytes(20).toString("hex"), - billingEmail: null, - image: null, }), + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + method: "POST", }); if (!res.ok) { const reason = await res.text(); console.error("failed to create team", { - status: res.status, reason, + status: res.status, }); switch (res.status) { case 400: { return { - status: "error", errorMessage: "Invalid team name or slug.", + status: "error", } as const; } case 401: { return { - status: "error", errorMessage: "You are not authorized to perform this action.", + status: "error", } as const; } default: { return { - status: "error", errorMessage: "An unknown error occurred.", + status: "error", } as const; } } @@ -69,7 +66,7 @@ export async function createTeam(options?: { }; return { - status: "success", data: json.result, + status: "success", } as const; } diff --git a/apps/dashboard/src/@/actions/deleteTeam.ts b/apps/dashboard/src/@/actions/deleteTeam.ts index bdfa2fe3f43..faf286883fa 100644 --- a/apps/dashboard/src/@/actions/deleteTeam.ts +++ b/apps/dashboard/src/@/actions/deleteTeam.ts @@ -3,63 +3,61 @@ import "server-only"; import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken"; import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "../constants/public-envs"; -export async function deleteTeam(options: { - teamId: string; -}) { +export async function deleteTeam(options: { teamId: string }) { const token = await getAuthToken(); if (!token) { return { - status: "error", errorMessage: "You are not authorized to perform this action.", + status: "error", } as const; } const res = await fetch( `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}`, { - method: "DELETE", headers: { Authorization: `Bearer ${token}`, }, + method: "DELETE", }, ); // handle errors if (!res.ok) { const reason = await res.text(); console.error("failed to delete team", { - status: res.status, reason, + status: res.status, }); switch (res.status) { case 400: { return { - status: "error", errorMessage: "Invalid team ID.", + status: "error", } as const; } case 401: { return { - status: "error", errorMessage: "You are not authorized to perform this action.", + status: "error", } as const; } case 403: { return { - status: "error", errorMessage: "You do not have permission to delete this team.", + status: "error", } as const; } case 404: { return { - status: "error", errorMessage: "Team not found.", + status: "error", } as const; } default: { return { - status: "error", errorMessage: "An unknown error occurred.", + status: "error", } as const; } } diff --git a/apps/dashboard/src/@/actions/getBalancesFromMoralis.ts b/apps/dashboard/src/@/actions/getBalancesFromMoralis.ts index 20559052c6b..ed34bbdd4d6 100644 --- a/apps/dashboard/src/@/actions/getBalancesFromMoralis.ts +++ b/apps/dashboard/src/@/actions/getBalancesFromMoralis.ts @@ -1,6 +1,6 @@ "use server"; import { defineDashboardChain } from "lib/defineDashboardChain"; -import { ZERO_ADDRESS, isAddress, toTokens } from "thirdweb"; +import { isAddress, toTokens, ZERO_ADDRESS } from "thirdweb"; import { getWalletBalance } from "thirdweb/wallets"; import { MORALIS_API_KEY } from "../constants/server-envs"; import { serverThirdwebClient } from "../constants/thirdweb-client.server"; @@ -43,12 +43,12 @@ export async function getTokenBalancesFromMoralis(params: { }); return [ { - token_address: ZERO_ADDRESS, - symbol: balance.symbol, - name: "Native Token", - decimals: balance.decimals, balance: balance.value.toString(), + decimals: balance.decimals, display_balance: toTokens(balance.value, balance.decimals), + name: "Native Token", + symbol: balance.symbol, + token_address: ZERO_ADDRESS, }, ]; }; @@ -59,10 +59,10 @@ export async function getTokenBalancesFromMoralis(params: { const tokenBalanceEndpoint = `https://deep-index.moralis.io/api/v2/${_address}/erc20?chain=${_chain}`; const resp = await fetch(tokenBalanceEndpoint, { - method: "GET", headers: { "x-api-key": MORALIS_API_KEY, }, + method: "GET", }); if (!resp.ok) { @@ -83,7 +83,7 @@ export async function getTokenBalancesFromMoralis(params: { ]); return { - error: undefined, data: [...nativeBalance, ...tokenBalances], + error: undefined, }; } diff --git a/apps/dashboard/src/@/actions/getWalletNFTs.ts b/apps/dashboard/src/@/actions/getWalletNFTs.ts index 291e4a820bf..c30ae6b7432 100644 --- a/apps/dashboard/src/@/actions/getWalletNFTs.ts +++ b/apps/dashboard/src/@/actions/getWalletNFTs.ts @@ -53,7 +53,7 @@ export async function getWalletNFTs(params: { const parsedResponse = await response.json(); const result = await transformAlchemyResponseToNFT(parsedResponse, owner); - return { result, error: undefined }; + return { error: undefined, result }; } catch (err) { console.error("Error fetching NFTs", err); return { error: "error parsing response" }; @@ -64,10 +64,10 @@ export async function getWalletNFTs(params: { const url = generateMoralisUrl({ chainId, owner }); const response = await fetch(url, { - method: "GET", headers: { "X-API-Key": MORALIS_API_KEY, }, + method: "GET", next: { revalidate: 10, // cache for 10 seconds }, @@ -156,8 +156,8 @@ async function getWalletNFTsFromInsight(params: { if (!response.ok) { const errorMessage = await response.text(); return { - ok: false, error: errorMessage, + ok: false, }; } @@ -172,32 +172,32 @@ async function getWalletNFTsFromInsight(params: { const walletNFTs = nftsResponse.data.map((nft) => { const walletNFT: WalletNFT = { - id: nft.token_id, + chainId: nft.contract.chain_id, contractAddress: nft.contract.address, + id: nft.token_id, metadata: { - uri: isDev - ? nft.metadata_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/") - : nft.metadata_url, - name: nft.name, - description: nft.description, - image: isDev - ? nft.image_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/") - : nft.image_url, animation_url: isDev ? nft.extra_metadata.animation_original_url?.replace( "ipfscdn.io/", "thirdwebstorage-dev.com/", ) : nft.extra_metadata.animation_original_url, - external_url: nft.external_url, background_color: nft.background_color, + description: nft.description, + external_url: nft.external_url, + image: isDev + ? nft.image_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/") + : nft.image_url, + name: nft.name, + uri: isDev + ? nft.metadata_url.replace("ipfscdn.io/", "thirdwebstorage-dev.com/") + : nft.metadata_url, }, owner: params.owner, - tokenURI: nft.metadata_url, - type: nft.token_type === "erc721" ? "ERC721" : "ERC1155", supply: nft.balance, tokenAddress: nft.contract.address, - chainId: nft.contract.chain_id, + tokenURI: nft.metadata_url, + type: nft.token_type === "erc721" ? "ERC721" : "ERC1155", }; return walletNFT; diff --git a/apps/dashboard/src/@/actions/proxies.ts b/apps/dashboard/src/@/actions/proxies.ts index 32df80874ca..83facc34d96 100644 --- a/apps/dashboard/src/@/actions/proxies.ts +++ b/apps/dashboard/src/@/actions/proxies.ts @@ -47,35 +47,35 @@ async function proxy( } const res = await fetch(url, { - method: params.method, + body: params.body, headers: { ...params.headers, ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}), }, - body: params.body, + method: params.method, }); if (!res.ok) { try { const errorMessage = await res.text(); return { - status: res.status, - ok: false, error: errorMessage || res.statusText, + ok: false, + status: res.status, }; } catch { return { - status: res.status, - ok: false, error: res.statusText, + ok: false, + status: res.status, }; } } return { - status: res.status, - ok: true, data: params.parseAsText ? await res.text() : await res.json(), + ok: true, + status: res.status, }; } diff --git a/apps/dashboard/src/@/actions/sendTeamInvite.ts b/apps/dashboard/src/@/actions/sendTeamInvite.ts index 36553726f79..a4e22d69fac 100644 --- a/apps/dashboard/src/@/actions/sendTeamInvite.ts +++ b/apps/dashboard/src/@/actions/sendTeamInvite.ts @@ -20,8 +20,8 @@ export async function sendTeamInvites(options: { if (!token) { return { - ok: false, errorMessage: "You are not authorized to perform this action", + ok: false, }; } @@ -43,15 +43,15 @@ async function sendInvite( const res = await fetch( `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamId}/invites`, { - method: "POST", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, body: JSON.stringify({ inviteEmail: invite.email, inviteRole: invite.role, }), + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + method: "POST", }, ); @@ -59,8 +59,8 @@ async function sendInvite( const errorMessage = await res.text(); return { email: invite.email, - ok: false, errorMessage, + ok: false, }; } diff --git a/apps/dashboard/src/@/actions/updateAccount.ts b/apps/dashboard/src/@/actions/updateAccount.ts index ab9f967520e..d37c7034f07 100644 --- a/apps/dashboard/src/@/actions/updateAccount.ts +++ b/apps/dashboard/src/@/actions/updateAccount.ts @@ -14,12 +14,12 @@ export async function updateAccount(values: { } const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/account`, { - method: "PUT", + body: JSON.stringify(values), headers: { - "Content-Type": "application/json", Authorization: `Bearer ${token}`, + "Content-Type": "application/json", }, - body: JSON.stringify(values), + method: "PUT", }); if (!res.ok) { diff --git a/apps/dashboard/src/@/analytics/hooks/identify-team.ts b/apps/dashboard/src/@/analytics/hooks/identify-team.ts index 80ae6da0b28..489ca357bd6 100644 --- a/apps/dashboard/src/@/analytics/hooks/identify-team.ts +++ b/apps/dashboard/src/@/analytics/hooks/identify-team.ts @@ -3,9 +3,7 @@ import posthog from "posthog-js"; import { useEffect } from "react"; -export function useIdentifyTeam(opts?: { - teamId: string; -}) { +export function useIdentifyTeam(opts?: { teamId: string }) { // eslint-disable-next-line no-restricted-syntax useEffect(() => { // if no teamId, don't identify diff --git a/apps/dashboard/src/@/analytics/report.ts b/apps/dashboard/src/@/analytics/report.ts index c579ccb2469..faf869a1e7f 100644 --- a/apps/dashboard/src/@/analytics/report.ts +++ b/apps/dashboard/src/@/analytics/report.ts @@ -115,9 +115,7 @@ export function reportOnboardingPlanSelectionSkipped() { * @jnsdls * */ -export function reportOnboardingMembersInvited(properties: { - count: number; -}) { +export function reportOnboardingMembersInvited(properties: { count: number }) { posthog.capture("onboarding members invited", { count: properties.count, }); @@ -185,9 +183,7 @@ export function reportOnboardingCompleted() { * @jnsdls * */ -export function reportFaucetUsed(properties: { - chainId: number; -}) { +export function reportFaucetUsed(properties: { chainId: number }) { posthog.capture("faucet used", { chainId: properties.chainId, }); @@ -217,8 +213,8 @@ export function reportChainConfigurationAdded(properties: { posthog.capture("chain configuration added", { chainId: properties.chainId, chainName: properties.chainName, - rpcURLs: properties.rpcURLs, nativeCurrency: properties.nativeCurrency, + rpcURLs: properties.rpcURLs, }); } @@ -242,9 +238,9 @@ export function reportAssetBuySuccessful(properties: { assetType: "nft" | "coin"; }) { posthog.capture("asset buy successful", { + assetType: properties.assetType, chainId: properties.chainId, contractType: properties.contractType, - assetType: properties.assetType, }); } @@ -263,9 +259,9 @@ export function reportAssetBuyFailed(properties: { error: string; }) { posthog.capture("asset buy failed", { + assetType: properties.assetType, chainId: properties.chainId, contractType: properties.contractType, - assetType: properties.assetType, error: properties.error, }); } diff --git a/apps/dashboard/src/@/api/analytics.ts b/apps/dashboard/src/@/api/analytics.ts index 751fc0ec9a5..230fe35a659 100644 --- a/apps/dashboard/src/@/api/analytics.ts +++ b/apps/dashboard/src/@/api/analytics.ts @@ -175,9 +175,9 @@ export async function getAggregateUserOpUsage( }, { date: (params.from || new Date()).toISOString(), - successful: 0, failed: 0, sponsoredUsd: 0, + successful: 0, }, ); } @@ -279,14 +279,14 @@ export async function isProjectActive(params: { ); return { bundler: false, - storage: false, - rpc: false, - nebula: false, - sdk: false, + ecosystemWallet: false, + inAppWallet: false, insight: false, + nebula: false, pay: false, - inAppWallet: false, - ecosystemWallet: false, + rpc: false, + sdk: false, + storage: false, } as ActiveStatus; } diff --git a/apps/dashboard/src/@/api/audit-log.ts b/apps/dashboard/src/@/api/audit-log.ts index 4ba20c14fc8..5e48374303c 100644 --- a/apps/dashboard/src/@/api/audit-log.ts +++ b/apps/dashboard/src/@/api/audit-log.ts @@ -58,34 +58,34 @@ export async function getAuditLogs(teamSlug: string, cursor?: string) { url.searchParams.set("take", "15"); const response = await fetch(url, { + headers: { + Authorization: `Bearer ${authToken}`, + }, next: { // revalidate this query once per 10 seconds (does not need to be more granular than that) revalidate: 10, }, - headers: { - Authorization: `Bearer ${authToken}`, - }, }); if (!response.ok) { // if the status is 402, the most likely reason is that the team is on a free plan if (response.status === 402) { return { - status: "error", reason: "higher_plan_required", + status: "error", } as const; } const body = await response.text(); return { - status: "error", - reason: "unknown", body, + reason: "unknown", + status: "error", } as const; } const data = (await response.json()) as AuditLogApiResponse; return { - status: "success", data, + status: "success", } as const; } diff --git a/apps/dashboard/src/@/api/insight/webhooks.ts b/apps/dashboard/src/@/api/insight/webhooks.ts index 1b14d0a790e..d992dc1a9b8 100644 --- a/apps/dashboard/src/@/api/insight/webhooks.ts +++ b/apps/dashboard/src/@/api/insight/webhooks.ts @@ -80,13 +80,13 @@ export async function createWebhook( const response = await fetch( `https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, { - method: "POST", + body: JSON.stringify(payload), headers: { + Authorization: `Bearer ${authToken}`, "Content-Type": "application/json", "x-client-id": clientId, - Authorization: `Bearer ${authToken}`, }, - body: JSON.stringify(payload), + method: "POST", }, ); @@ -115,11 +115,11 @@ export async function getWebhooks( const response = await fetch( `https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks`, { - method: "GET", headers: { - "x-client-id": clientId, Authorization: `Bearer ${authToken}`, + "x-client-id": clientId, }, + method: "GET", }, ); @@ -149,11 +149,11 @@ export async function deleteWebhook( const response = await fetch( `https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/${encodeURIComponent(webhookId)}`, { - method: "DELETE", headers: { - "x-client-id": clientId, Authorization: `Bearer ${authToken}`, + "x-client-id": clientId, }, + method: "DELETE", }, ); @@ -183,29 +183,29 @@ export async function testWebhook( const response = await fetch( `https://${THIRDWEB_INSIGHT_API_DOMAIN}/v1/webhooks/test`, { - method: "POST", + body: JSON.stringify(payload), headers: { + Authorization: `Bearer ${authToken}`, "Content-Type": "application/json", "x-client-id": clientId, - Authorization: `Bearer ${authToken}`, }, - body: JSON.stringify(payload), + method: "POST", }, ); if (!response.ok) { const errorText = await response.text(); return { - success: false, error: `Failed to test webhook: ${errorText}`, + success: false, }; } return (await response.json()) as TestWebhookResponse; } catch (error) { return { - success: false, error: `Network or parsing error: ${error instanceof Error ? error.message : "Unknown error"}`, + success: false, }; } } @@ -215,10 +215,10 @@ export async function getSupportedWebhookChains(): Promise | undefined; }; -export async function getPaymentLink(props: { - paymentId: string; -}) { +export async function getPaymentLink(props: { paymentId: string }) { const res = await fetch(`${UB_BASE_URL}/v1/links/${props.paymentId}`, { - method: "GET", headers: { "Content-Type": "application/json", "x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY, }, + method: "GET", }); if (!res.ok) { @@ -36,12 +34,12 @@ export async function getPaymentLink(props: { const { data } = await res.json(); return { + amount: data.amount ? BigInt(data.amount) : undefined, clientId: data.clientId, - title: data.title, - imageUrl: data.imageUrl, - receiver: data.receiver, destinationToken: data.destinationToken, - amount: data.amount ? BigInt(data.amount) : undefined, + imageUrl: data.imageUrl, purchaseData: data.purchaseData, + receiver: data.receiver, + title: data.title, } as PaymentLink; } diff --git a/apps/dashboard/src/@/api/universal-bridge/tokens.ts b/apps/dashboard/src/@/api/universal-bridge/tokens.ts index a7e60b5d884..2dc46823d64 100644 --- a/apps/dashboard/src/@/api/universal-bridge/tokens.ts +++ b/apps/dashboard/src/@/api/universal-bridge/tokens.ts @@ -1,7 +1,7 @@ "use server"; -import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs"; import type { ProjectResponse } from "@thirdweb-dev/service-utils"; import { getAuthToken } from "app/(app)/api/lib/getAuthToken"; +import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs"; import { UB_BASE_URL } from "./constants"; export type TokenMetadata = { @@ -13,9 +13,7 @@ export type TokenMetadata = { iconUri?: string; }; -export async function getUniversalBridgeTokens(props: { - chainId?: number; -}) { +export async function getUniversalBridgeTokens(props: { chainId?: number }) { const url = new URL(`${UB_BASE_URL}/v1/tokens`); if (props.chainId) { @@ -24,11 +22,11 @@ export async function getUniversalBridgeTokens(props: { url.searchParams.append("limit", "1000"); const res = await fetch(url.toString(), { - method: "GET", headers: { "Content-Type": "application/json", "x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY, } as Record, + method: "GET", }); if (!res.ok) { @@ -49,16 +47,16 @@ export async function addUniversalBridgeTokenRoute(props: { const url = new URL(`${UB_BASE_URL}/v1/tokens`); const res = await fetch(url.toString(), { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${authToken}`, - "x-client-id": props.project.publishableKey, - } as Record, body: JSON.stringify({ chainId: props.chainId, tokenAddress: props.tokenAddress, }), + headers: { + Authorization: `Bearer ${authToken}`, + "Content-Type": "application/json", + "x-client-id": props.project.publishableKey, + } as Record, + method: "POST", }); if (!res.ok) { diff --git a/apps/dashboard/src/@/api/usage/billing-preview.ts b/apps/dashboard/src/@/api/usage/billing-preview.ts index 70e5e1a0eea..bc4cf815c61 100644 --- a/apps/dashboard/src/@/api/usage/billing-preview.ts +++ b/apps/dashboard/src/@/api/usage/billing-preview.ts @@ -34,33 +34,33 @@ export async function getBilledUsage(teamSlug: string) { NEXT_PUBLIC_THIRDWEB_API_HOST, ), { + headers: { + Authorization: `Bearer ${authToken}`, + }, next: { // revalidate this query once per minute (does not need to be more granular than that) revalidate: 60, }, - headers: { - Authorization: `Bearer ${authToken}`, - }, }, ); if (!response.ok) { // if the status is 404, the most likely reason is that the team is on a free plan if (response.status === 404) { return { - status: "error", reason: "free_plan", + status: "error", } as const; } const body = await response.text(); return { - status: "error", - reason: "unknown", body, + reason: "unknown", + status: "error", } as const; } const data = (await response.json()) as UsageApiResponse; return { - status: "success", data, + status: "success", } as const; } diff --git a/apps/dashboard/src/@/api/usage/rpc.ts b/apps/dashboard/src/@/api/usage/rpc.ts index 4d9439170ff..37a39d24504 100644 --- a/apps/dashboard/src/@/api/usage/rpc.ts +++ b/apps/dashboard/src/@/api/usage/rpc.ts @@ -23,11 +23,7 @@ type Last24HoursRPCUsageApiResponse = { }; export const getLast24HoursRPCUsage = unstable_cache( - async (params: { - teamId: string; - projectId?: string; - authToken: string; - }) => { + async (params: { teamId: string; projectId?: string; authToken: string }) => { const analyticsEndpoint = ANALYTICS_SERVICE_URL; const url = new URL(`${analyticsEndpoint}/v2/rpc/24-hours`); url.searchParams.set("teamId", params.teamId); @@ -44,16 +40,16 @@ export const getLast24HoursRPCUsage = unstable_cache( if (!res.ok) { const error = await res.text(); return { - ok: false as const, error: error, + ok: false as const, }; } const resData = await res.json(); return { - ok: true as const, data: resData.data as Last24HoursRPCUsageApiResponse, + ok: true as const, }; }, ["rpc-usage-last-24-hours:v2"], diff --git a/apps/dashboard/src/@/api/verified-domain.ts b/apps/dashboard/src/@/api/verified-domain.ts index 26d2c61f3f4..5fc17fa39bd 100644 --- a/apps/dashboard/src/@/api/verified-domain.ts +++ b/apps/dashboard/src/@/api/verified-domain.ts @@ -56,12 +56,12 @@ export async function createDomainVerification( const res = await fetch( `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${teamIdOrSlug}/verified-domain`, { - method: "POST", body: JSON.stringify({ domain }), headers: { - "Content-Type": "application/json", Authorization: `Bearer ${token}`, + "Content-Type": "application/json", }, + method: "POST", }, ); diff --git a/apps/dashboard/src/@/components/ChakraProviderSetup.tsx b/apps/dashboard/src/@/components/ChakraProviderSetup.tsx index 43556ee2686..d934c8c4f59 100644 --- a/apps/dashboard/src/@/components/ChakraProviderSetup.tsx +++ b/apps/dashboard/src/@/components/ChakraProviderSetup.tsx @@ -5,9 +5,7 @@ import { useTheme } from "next-themes"; import { useEffect } from "react"; import chakraTheme from "../../theme"; -export function ChakraProviderSetup(props: { - children: React.ReactNode; -}) { +export function ChakraProviderSetup(props: { children: React.ReactNode }) { return ( {props.children} diff --git a/apps/dashboard/src/@/components/Responsive.tsx b/apps/dashboard/src/@/components/Responsive.tsx index 6b78fc76779..382258f927d 100644 --- a/apps/dashboard/src/@/components/Responsive.tsx +++ b/apps/dashboard/src/@/components/Responsive.tsx @@ -1,6 +1,6 @@ "use client"; -import { ClientOnly } from "@/components/blocks/client-only"; import { Suspense } from "react"; +import { ClientOnly } from "@/components/blocks/client-only"; import { useIsMobile } from "../hooks/use-mobile"; export function ResponsiveLayout(props: { diff --git a/apps/dashboard/src/@/components/billing.tsx b/apps/dashboard/src/@/components/billing.tsx index 52384a3fcc4..fbb30634b53 100644 --- a/apps/dashboard/src/@/components/billing.tsx +++ b/apps/dashboard/src/@/components/billing.tsx @@ -38,12 +38,12 @@ export function CheckoutButton(props: { }} > {props.children} @@ -59,8 +59,8 @@ function BillingWarning({ teamSlug }: { teamSlug: string }) {

You have outstanding invoices. Please{" "} pay them {" "} @@ -78,17 +78,17 @@ export function BillingPortalButton(props: { return ( @@ -120,19 +120,19 @@ function ToggleTest() { diff --git a/apps/dashboard/src/@/components/blocks/Avatars/GradientAvatar.tsx b/apps/dashboard/src/@/components/blocks/Avatars/GradientAvatar.tsx index e0c411d6070..195a2e1243b 100644 --- a/apps/dashboard/src/@/components/blocks/Avatars/GradientAvatar.tsx +++ b/apps/dashboard/src/@/components/blocks/Avatars/GradientAvatar.tsx @@ -1,5 +1,5 @@ -import { Img } from "@/components/blocks/Img"; import type { ThirdwebClient } from "thirdweb"; +import { Img } from "@/components/blocks/Img"; import { resolveSchemeWithErrorHandler } from "../../../lib/resolveSchemeWithErrorHandler"; import { cn } from "../../../lib/utils"; import { GradientBlobbie } from "./GradientBlobbie"; @@ -19,9 +19,9 @@ export function GradientAvatar(props: { return ( : undefined} + src={resolvedSrc} /> ); } diff --git a/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.stories.tsx b/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.stories.tsx index 11ee5218c0c..5bb27b423dd 100644 --- a/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.stories.tsx +++ b/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.stories.tsx @@ -8,9 +8,9 @@ import { Button } from "../../ui/button"; import { ProjectAvatar } from "./ProjectAvatar"; const meta = { - title: "blocks/Avatars/ProjectAvatar", component: Story, parameters: {}, + title: "blocks/Avatars/ProjectAvatar", } satisfies Meta; export default meta; @@ -27,17 +27,17 @@ function Story() { @@ -54,7 +54,7 @@ function ToggleTest() { return (

@@ -74,17 +74,17 @@ function ToggleTest() {
diff --git a/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.tsx b/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.tsx index 73d917c4107..cfd250feb54 100644 --- a/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.tsx +++ b/apps/dashboard/src/@/components/blocks/Avatars/ProjectAvatar.tsx @@ -1,6 +1,6 @@ -import { Img } from "@/components/blocks/Img"; import { BoxIcon } from "lucide-react"; import type { ThirdwebClient } from "thirdweb"; +import { Img } from "@/components/blocks/Img"; import { resolveSchemeWithErrorHandler } from "../../../lib/resolveSchemeWithErrorHandler"; import { cn } from "../../../lib/utils"; @@ -11,19 +11,19 @@ export function ProjectAvatar(props: { }) { return ( {""} } + src={ + resolveSchemeWithErrorHandler({ + client: props.client, + uri: props.src, + }) || "" + } /> ); } diff --git a/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx b/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx index dfe4652eb72..8ab5bcb0dc1 100644 --- a/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx +++ b/apps/dashboard/src/@/components/blocks/DangerSettingCard.stories.tsx @@ -3,13 +3,13 @@ import { BadgeContainer } from "../../../stories/utils"; import { DangerSettingCard } from "./DangerSettingCard"; const meta = { - title: "blocks/Cards/DangerSettingCard", component: Story, parameters: { nextjs: { appDirectory: true, }, }, + title: "blocks/Cards/DangerSettingCard", } satisfies Meta; export default meta; @@ -24,29 +24,29 @@ function Story() {
{}} - isPending={false} confirmationDialog={{ - title: "This is confirmation title", description: "This is confirmation description", + title: "This is confirmation title", }} + description="This is a description" + isPending={false} + title="This is a title" /> {}} - isPending={true} confirmationDialog={{ - title: "This is confirmation title", description: "This is confirmation description", + title: "This is confirmation title", }} + description="This is a description" + isPending={true} + title="This is a title" />
diff --git a/apps/dashboard/src/@/components/blocks/DangerSettingCard.tsx b/apps/dashboard/src/@/components/blocks/DangerSettingCard.tsx index 74e5b43bd7c..fbd8018d939 100644 --- a/apps/dashboard/src/@/components/blocks/DangerSettingCard.tsx +++ b/apps/dashboard/src/@/components/blocks/DangerSettingCard.tsx @@ -1,4 +1,4 @@ -import { Spinner } from "@/components/ui/Spinner/Spinner"; +import { useState } from "react"; import { Button } from "@/components/ui/button"; import { @@ -10,7 +10,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { useState } from "react"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { cn } from "../../lib/utils"; import { DynamicHeight } from "../ui/DynamicHeight"; @@ -58,19 +58,19 @@ export function DangerSettingCard(props: { )} > { setIsConfirmationDialogOpen(v); if (!v) { props.confirmationDialog.onClose?.(); } }} + open={isConfirmationDialogOpen} > diff --git a/apps/dashboard/src/@/components/blocks/Img.tsx b/apps/dashboard/src/@/components/blocks/Img.tsx index e1fcc37142a..2884cf09e58 100644 --- a/apps/dashboard/src/@/components/blocks/Img.tsx +++ b/apps/dashboard/src/@/components/blocks/Img.tsx @@ -53,30 +53,30 @@ export function Img(props: imgElementProps) { {restProps.alt { setStatus("fallback"); }} + ref={imgRef} + src={restProps.src || undefined} style={{ opacity: status === "loaded" ? 1 : 0, ...restProps.style, }} - alt={restProps.alt || ""} - className={cn( - "fade-in-0 object-cover transition-opacity duration-300", - className, - )} - decoding="async" /> {status !== "loaded" && (
*]:h-full [&>*]:w-full", className, )} + style={restProps.style} > {status === "pending" && (skeleton || defaultSkeleton)} {status === "fallback" && (fallback || defaultFallback)} diff --git a/apps/dashboard/src/@/components/blocks/MobileSidebar.tsx b/apps/dashboard/src/@/components/blocks/MobileSidebar.tsx index 0e469c295ce..6d38e34524a 100644 --- a/apps/dashboard/src/@/components/blocks/MobileSidebar.tsx +++ b/apps/dashboard/src/@/components/blocks/MobileSidebar.tsx @@ -1,10 +1,10 @@ "use client"; -import { Button } from "@/components/ui/button"; -import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { ChevronDownIcon } from "lucide-react"; import { usePathname } from "next/navigation"; import { useMemo, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; import { cn } from "../../lib/utils"; import { RenderSidebarLinks, @@ -35,7 +35,7 @@ export function MobileSidebar(props: { ); return ( - + {props.trigger || defaultTrigger} ; export default meta; @@ -35,17 +35,14 @@ function Story() { ); } -function Variant(props: { - label: string; - selectedChainIds: number[]; -}) { +function Variant(props: { label: string; selectedChainIds: number[] }) { const [chainIds, setChainIds] = useState(props.selectedChainIds); return ( ); diff --git a/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx b/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx index 5c46846fc54..0c0fdce92d6 100644 --- a/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx +++ b/apps/dashboard/src/@/components/blocks/NetworkSelectors.tsx @@ -1,12 +1,12 @@ "use client"; -import { MultiSelect } from "@/components/blocks/multi-select"; -import { SelectWithSearch } from "@/components/blocks/select-with-search"; -import { Badge } from "@/components/ui/badge"; import { ChainIconClient } from "components/icons/ChainIcon"; import { useAllChainsData } from "hooks/chains/allChains"; import { useCallback, useMemo } from "react"; import type { ThirdwebClient } from "thirdweb"; +import { MultiSelect } from "@/components/blocks/multi-select"; +import { SelectWithSearch } from "@/components/blocks/select-with-search"; +import { Badge } from "@/components/ui/badge"; function cleanChainName(chainName: string) { return chainName.replace("Mainnet", ""); @@ -98,15 +98,15 @@ export function MultiNetworkSelector(props: { {cleanChainName(chain.name)} {!props.disableChainId && ( - + Chain ID {chain.chainId} @@ -119,24 +119,24 @@ export function MultiNetworkSelector(props: { return ( { props.onChange(chainIds.map(Number)); }} + options={options} + overrideSearchFn={searchFn} placeholder={ allChains.length === 0 ? "Loading Chains..." : "Select Chains" } - disabled={allChains.length === 0} - overrideSearchFn={searchFn} + popoverContentClassName={props.popoverContentClassName} renderOption={renderOption} - className={props.className} + searchPlaceholder="Search by Name or Chain Id" + selectedValues={props.selectedChainIds.map(String)} + showSelectedValuesInModal={props.showSelectedValuesInModal} + side={props.side} /> ); } @@ -208,15 +208,15 @@ export function SingleNetworkSelector(props: { {cleanChainName(chain.name)} {!props.disableChainId && ( - + Chain ID {chain.chainId} @@ -231,26 +231,26 @@ export function SingleNetworkSelector(props: { return ( { props.onChange(Number(chainId)); }} - closeOnSelect={true} + options={options} + overrideSearchFn={searchFn} placeholder={ isLoadingChains ? "Loading Chains..." : props.placeholder || "Select Chain" } - overrideSearchFn={searchFn} - renderOption={renderOption} - className={props.className} popoverContentClassName={props.popoverContentClassName} - disabled={isLoadingChains} + renderOption={renderOption} + searchPlaceholder="Search by Name or Chain ID" + showCheck={false} side={props.side} - align={props.align} + value={String(props.chainId)} /> ); } diff --git a/apps/dashboard/src/@/components/blocks/RouteDiscoveryCard.tsx b/apps/dashboard/src/@/components/blocks/RouteDiscoveryCard.tsx index 2afdd28fa3c..e2d12445fca 100644 --- a/apps/dashboard/src/@/components/blocks/RouteDiscoveryCard.tsx +++ b/apps/dashboard/src/@/components/blocks/RouteDiscoveryCard.tsx @@ -1,7 +1,7 @@ -import { Spinner } from "@/components/ui/Spinner/Spinner"; +import type React from "react"; import { Button } from "@/components/ui/button"; +import { Spinner } from "@/components/ui/Spinner/Spinner"; import { cn } from "@/lib/utils"; -import type React from "react"; export function RouteDiscoveryCard( props: React.PropsWithChildren<{ @@ -66,12 +66,12 @@ export function RouteDiscoveryCard( {props.saveButton && !props.noPermissionText && (
- - - - - - -
); diff --git a/apps/dashboard/src/@/components/blocks/code-segment.stories.tsx b/apps/dashboard/src/@/components/blocks/code-segment.stories.tsx index 09fb395e000..2bf0c3d94c1 100644 --- a/apps/dashboard/src/@/components/blocks/code-segment.stories.tsx +++ b/apps/dashboard/src/@/components/blocks/code-segment.stories.tsx @@ -1,3 +1,6 @@ +import type { Meta, StoryObj } from "@storybook/nextjs"; +import { useState } from "react"; +import { BadgeContainer } from "stories/utils"; import { Select, SelectContent, @@ -6,19 +9,16 @@ import { SelectTrigger, SelectValue, } from "@/components/ui/select"; -import type { Meta, StoryObj } from "@storybook/nextjs"; -import { useState } from "react"; -import { BadgeContainer } from "stories/utils"; import { type CodeEnvironment, CodeSegment } from "./code-segment.client"; const meta = { - title: "blocks/CodeSegment", component: Story, parameters: { nextjs: { appDirectory: true, }, }, + title: "blocks/CodeSegment", } satisfies Meta; export default meta; @@ -37,10 +37,10 @@ function Story() { return (
- +
); @@ -92,23 +92,23 @@ function Variant(props: { return ( ); diff --git a/apps/dashboard/src/@/components/blocks/dismissible-alert.tsx b/apps/dashboard/src/@/components/blocks/dismissible-alert.tsx index daa89d1025a..3bc132f3b93 100644 --- a/apps/dashboard/src/@/components/blocks/dismissible-alert.tsx +++ b/apps/dashboard/src/@/components/blocks/dismissible-alert.tsx @@ -20,9 +20,9 @@ export function DismissibleAlert(props: { return (
diff --git a/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx b/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx index 2642f46882f..a14d18e0afb 100644 --- a/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx +++ b/apps/dashboard/src/@/components/blocks/error-fallbacks/unexpect-value-error-message.tsx @@ -1,6 +1,6 @@ +import { useMemo } from "react"; import { CopyTextButton } from "@/components/ui/CopyTextButton"; import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow"; -import { useMemo } from "react"; export function UnexpectedValueErrorMessage(props: { value: unknown; @@ -30,11 +30,11 @@ export function UnexpectedValueErrorMessage(props: {
diff --git a/apps/dashboard/src/@/components/blocks/multi-select.stories.tsx b/apps/dashboard/src/@/components/blocks/multi-select.stories.tsx index 816ae7ca763..0c3af5cc055 100644 --- a/apps/dashboard/src/@/components/blocks/multi-select.stories.tsx +++ b/apps/dashboard/src/@/components/blocks/multi-select.stories.tsx @@ -4,13 +4,13 @@ import { BadgeContainer } from "../../../stories/utils"; import { MultiSelect } from "./multi-select"; const meta = { - title: "blocks/MultiSelect", component: Story, parameters: { nextjs: { appDirectory: true, }, }, + title: "blocks/MultiSelect", } satisfies Meta; export default meta; @@ -22,26 +22,26 @@ export const Variants: Story = { function createList(len: number) { return Array.from({ length: len }, (_, i) => ({ - value: `${i}`, label: `Item ${i}`, + value: `${i}`, })); } function Story() { return (
- - + +
); @@ -59,13 +59,13 @@ function VariantTest(props: { return ( { setValues(values); }} + options={list} placeholder="Select items" - maxCount={props.maxCount} + selectedValues={values} /> ); diff --git a/apps/dashboard/src/@/components/blocks/multi-select.tsx b/apps/dashboard/src/@/components/blocks/multi-select.tsx index b50ec0befc9..9a2c118c877 100644 --- a/apps/dashboard/src/@/components/blocks/multi-select.tsx +++ b/apps/dashboard/src/@/components/blocks/multi-select.tsx @@ -1,6 +1,8 @@ -/* eslint-disable no-restricted-syntax */ +/** biome-ignore-all lint/a11y/useSemanticElements: FIXME */ "use client"; +import { CheckIcon, ChevronDownIcon, SearchIcon, XIcon } from "lucide-react"; +import { forwardRef, useCallback, useMemo, useRef, useState } from "react"; import { Button } from "@/components/ui/button"; import { Popover, @@ -9,18 +11,9 @@ import { } from "@/components/ui/popover"; import { Separator } from "@/components/ui/separator"; import { cn } from "@/lib/utils"; -import { CheckIcon, ChevronDownIcon, SearchIcon, XIcon } from "lucide-react"; -import { - forwardRef, - useCallback, - useEffect, - useMemo, - useRef, - useState, -} from "react"; import { useShowMore } from "../../lib/useShowMore"; -import { ScrollShadow } from "../ui/ScrollShadow/ScrollShadow"; import { Input } from "../ui/input"; +import { ScrollShadow } from "../ui/ScrollShadow/ScrollShadow"; interface MultiSelectProps extends React.ButtonHTMLAttributes { @@ -150,58 +143,48 @@ export const MultiSelect = forwardRef( // scroll to top when options change const popoverElRef = useRef(null); - // biome-ignore lint/correctness/useExhaustiveDependencies: - useEffect(() => { - const scrollContainer = - popoverElRef.current?.querySelector("[data-scrollable]"); - if (scrollContainer) { - scrollContainer.scrollTo({ - top: 0, - }); - } - }, [searchValue]); return ( - + {props.customTrigger || ( diff --git a/apps/dashboard/src/@/components/blocks/notifications/notification-list.tsx b/apps/dashboard/src/@/components/blocks/notifications/notification-list.tsx index 476c985f172..b13d46130e3 100644 --- a/apps/dashboard/src/@/components/blocks/notifications/notification-list.tsx +++ b/apps/dashboard/src/@/components/blocks/notifications/notification-list.tsx @@ -1,7 +1,7 @@ "use client"; -import { TabButtons } from "@/components/ui/tabs"; import { ArchiveIcon, BellIcon, Loader2Icon } from "lucide-react"; import { useEffect, useRef, useState } from "react"; +import { TabButtons } from "@/components/ui/tabs"; import { Badge } from "../../ui/badge"; import { NotificationEntry } from "./notification-entry"; import type { useNotifications } from "./state/manager"; @@ -23,10 +23,11 @@ export function NotificationList(props: ReturnType) { return (
Inbox @@ -38,12 +39,11 @@ export function NotificationList(props: ReturnType) {
), onClick: () => setActiveTab("inbox"), - isActive: activeTab === "inbox", }, { + isActive: activeTab === "archive", name: "Archive", onClick: () => setActiveTab("archive"), - isActive: activeTab === "archive", }, ]} /> @@ -54,21 +54,21 @@ export function NotificationList(props: ReturnType) { > {activeTab === "inbox" ? ( ) : ( @@ -110,8 +110,8 @@ function InboxTab( ))} @@ -144,8 +144,8 @@ function ArchiveTab( ))} @@ -198,5 +198,5 @@ function AutoLoadMore(props: { ); } - return
; + return
; } diff --git a/apps/dashboard/src/@/components/blocks/notifications/state/manager.ts b/apps/dashboard/src/@/components/blocks/notifications/state/manager.ts index 0def2459717..390770b8636 100644 --- a/apps/dashboard/src/@/components/blocks/notifications/state/manager.ts +++ b/apps/dashboard/src/@/components/blocks/notifications/state/manager.ts @@ -1,13 +1,5 @@ "use client"; -import { - type Notification, - type NotificationsApiResponse, - getArchivedNotifications, - getUnreadNotifications, - getUnreadNotificationsCount, - markNotificationAsRead, -} from "@/api/notifications"; import { type InfiniteData, useInfiniteQuery, @@ -15,9 +7,16 @@ import { useQuery, useQueryClient, } from "@tanstack/react-query"; - import { useMemo } from "react"; import { toast } from "sonner"; +import { + getArchivedNotifications, + getUnreadNotifications, + getUnreadNotificationsCount, + markNotificationAsRead, + type Notification, + type NotificationsApiResponse, +} from "@/api/notifications"; /** * Internal helper to safely flatten pages coming from useInfiniteQuery. @@ -68,7 +67,9 @@ export function useNotifications(accountId: string) { ); const unreadQuery = useInfiniteQuery({ - queryKey: unreadQueryKey, + enabled: !!accountId, + getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined, + initialPageParam: undefined as string | undefined, queryFn: async ({ pageParam }) => { const cursor = (pageParam ?? undefined) as string | undefined; const res = await getUnreadNotifications(cursor); @@ -77,14 +78,14 @@ export function useNotifications(accountId: string) { } return res.data; }, - initialPageParam: undefined as string | undefined, - getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined, - enabled: !!accountId, + queryKey: unreadQueryKey, refetchInterval: 60_000, // 1min }); const archivedQuery = useInfiniteQuery({ - queryKey: archivedQueryKey, + enabled: !!accountId, + getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined, + initialPageParam: undefined as string | undefined, queryFn: async ({ pageParam }) => { const cursor = (pageParam ?? undefined) as string | undefined; const res = await getArchivedNotifications(cursor); @@ -93,14 +94,12 @@ export function useNotifications(accountId: string) { } return res.data; }, - initialPageParam: undefined as string | undefined, - getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined, - enabled: !!accountId, + queryKey: archivedQueryKey, refetchInterval: 60_000, // 1min }); const unreadCountQuery = useQuery({ - queryKey: unreadCountKey, + enabled: !!accountId, queryFn: async () => { const res = await getUnreadNotificationsCount(); if (res.status === "error") { @@ -108,8 +107,8 @@ export function useNotifications(accountId: string) { } return res.data.result.unreadCount; }, - refetchInterval: 60_000, // 1min - enabled: !!accountId, + queryKey: unreadCountKey, // 1min + refetchInterval: 60_000, }); // -------------------- @@ -172,16 +171,7 @@ export function useNotifications(accountId: string) { queryClient.setQueryData(unreadCountKey, optimisticCount); } - return { previousUnread, previousCount } as const; - }, - // Rollback on error - onError: (_err, _vars, context) => { - if (context?.previousUnread) { - queryClient.setQueryData(unreadQueryKey, context.previousUnread); - } - if (typeof context?.previousCount === "number") { - queryClient.setQueryData(unreadCountKey, context.previousCount); - } + return { previousCount, previousUnread } as const; }, // Always refetch to ensure consistency onSettled: () => { @@ -199,25 +189,25 @@ export function useNotifications(accountId: string) { const unreadNotificationsCount = unreadCountQuery.data ?? 0; // this is the total unread count return { - // data - unreadNotifications, archivedNotifications, - unreadNotificationsCount, + hasMoreArchived: archivedQuery.hasNextPage ?? false, + hasMoreUnread: unreadQuery.hasNextPage ?? false, + isFetchingMoreArchived: archivedQuery.isFetchingNextPage, + isFetchingMoreUnread: unreadQuery.isFetchingNextPage, + isLoadingArchived: archivedQuery.isLoading, // booleans isLoadingUnread: unreadQuery.isLoading, - isLoadingArchived: archivedQuery.isLoading, - hasMoreUnread: unreadQuery.hasNextPage ?? false, - hasMoreArchived: archivedQuery.hasNextPage ?? false, - isFetchingMoreUnread: unreadQuery.isFetchingNextPage, - isFetchingMoreArchived: archivedQuery.isFetchingNextPage, + loadMoreArchived: () => archivedQuery.fetchNextPage(), // pagination helpers loadMoreUnread: () => unreadQuery.fetchNextPage(), - loadMoreArchived: () => archivedQuery.fetchNextPage(), + markAllAsRead: () => archiveMutation.mutate(undefined), // mutations markAsRead: (id: string) => archiveMutation.mutate(id), - markAllAsRead: () => archiveMutation.mutate(undefined), + // data + unreadNotifications, + unreadNotificationsCount, } as const; } diff --git a/apps/dashboard/src/@/components/blocks/pricing-card.tsx b/apps/dashboard/src/@/components/blocks/pricing-card.tsx index d947fe59602..f8e51be3537 100644 --- a/apps/dashboard/src/@/components/blocks/pricing-card.tsx +++ b/apps/dashboard/src/@/components/blocks/pricing-card.tsx @@ -1,15 +1,15 @@ "use client"; -import type { Team } from "@/api/team"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { ToolTipLabel } from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; import { RenewSubscriptionButton } from "components/settings/Account/Billing/renew-subscription/renew-subscription-button"; import { CheckIcon, DollarSignIcon } from "lucide-react"; import Link from "next/link"; import type React from "react"; import { remainingDays } from "utils/date-utils"; import { TEAM_PLANS } from "utils/pricing"; +import type { Team } from "@/api/team"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { ToolTipLabel } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; import type { ProductSKU } from "../../lib/billing"; import { CheckoutButton } from "../billing"; @@ -141,18 +141,18 @@ export const PricingCard: React.FC = ({ {cta && (
{cta.type === "renew" && ( - + )} {billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && ( {has7DayTrial ? "Start 7 Day Free Trial" : cta.label} @@ -160,15 +160,15 @@ export const PricingCard: React.FC = ({ {cta.type === "link" && ( -
diff --git a/apps/dashboard/src/@/components/color-mode-toggle.tsx b/apps/dashboard/src/@/components/color-mode-toggle.tsx index 41dc2be94ba..69fa420315d 100644 --- a/apps/dashboard/src/@/components/color-mode-toggle.tsx +++ b/apps/dashboard/src/@/components/color-mode-toggle.tsx @@ -1,9 +1,9 @@ "use client"; -import { ClientOnly } from "@/components/blocks/client-only"; -import { Button } from "@/components/ui/button"; import { MoonIcon, SunIcon } from "lucide-react"; import { useTheme } from "next-themes"; +import { ClientOnly } from "@/components/blocks/client-only"; +import { Button } from "@/components/ui/button"; import { Skeleton } from "./ui/skeleton"; export function ToggleThemeButton() { @@ -14,12 +14,12 @@ export function ToggleThemeButton() { ssr={} > diff --git a/apps/dashboard/src/@/components/ui/ConfirmationDialog.tsx b/apps/dashboard/src/@/components/ui/ConfirmationDialog.tsx index 71fc93fa063..1df6a543225 100644 --- a/apps/dashboard/src/@/components/ui/ConfirmationDialog.tsx +++ b/apps/dashboard/src/@/components/ui/ConfirmationDialog.tsx @@ -1,3 +1,5 @@ +import type { VariantProps } from "class-variance-authority"; +import type React from "react"; import { AlertDialog, AlertDialogAction, @@ -11,8 +13,6 @@ import { } from "@/components/ui/alert-dialog"; import type { buttonVariants } from "@/components/ui/button"; import { cn } from "@/lib/utils"; -import type { VariantProps } from "class-variance-authority"; -import type React from "react"; export interface ConfirmationDialogProps extends VariantProps { @@ -36,7 +36,7 @@ export function ConfirmationDialog({ className, }: ConfirmationDialogProps) { return ( - + {children} @@ -48,12 +48,12 @@ export function ConfirmationDialog({ Cancel { onSubmit?.(); onOpenChange?.(false); }} + type="submit" + variant={variant} > Continue diff --git a/apps/dashboard/src/@/components/ui/CopyAddressButton.tsx b/apps/dashboard/src/@/components/ui/CopyAddressButton.tsx index 22115ff1f03..03941891434 100644 --- a/apps/dashboard/src/@/components/ui/CopyAddressButton.tsx +++ b/apps/dashboard/src/@/components/ui/CopyAddressButton.tsx @@ -20,12 +20,12 @@ export function CopyAddressButton(props: { return ( ); } diff --git a/apps/dashboard/src/@/components/ui/CopyButton.tsx b/apps/dashboard/src/@/components/ui/CopyButton.tsx index ffa411fa5c8..4e46643cbd7 100644 --- a/apps/dashboard/src/@/components/ui/CopyButton.tsx +++ b/apps/dashboard/src/@/components/ui/CopyButton.tsx @@ -1,8 +1,8 @@ "use client"; -import { cn } from "@/lib/utils"; import { useClipboard } from "hooks/useClipboard"; import { CheckIcon, CopyIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; import { Button } from "./button"; import { ToolTipLabel } from "./tooltip"; @@ -16,10 +16,10 @@ export function CopyButton(props: { return ( - - - - -