biome 2 (#7398)
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Vendored
+26
-27
@@ -1,11 +1,32 @@
|
||||
{
|
||||
"editor.formatOnSave": true,
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.fixAll.eslint": "explicit",
|
||||
"source.organizeImports.biome": "explicit",
|
||||
"quickfix.biome": "explicit"
|
||||
"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"
|
||||
],
|
||||
@@ -15,27 +36,5 @@
|
||||
"next/dist",
|
||||
"^lucide-react/dist/lucide-react.suffixed$"
|
||||
],
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[markdown]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"[css]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"eslint.workingDirectories": [
|
||||
{ "pattern": "./packages/*/" },
|
||||
{ "pattern": "./apps/*/" }
|
||||
]
|
||||
"typescript.tsdk": "node_modules/typescript/lib"
|
||||
}
|
||||
|
||||
+122
-114
@@ -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",
|
||||
],
|
||||
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-explicit-any": "off",
|
||||
"@typescript-eslint/no-non-null-assertion": "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",
|
||||
},
|
||||
},
|
||||
// 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: {
|
||||
"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".',
|
||||
},
|
||||
{
|
||||
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.',
|
||||
},
|
||||
{
|
||||
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:
|
||||
"Use useV5DashboardChain instead if you are using it inside a component",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
],
|
||||
"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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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": "//"
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/global.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rsc": true,
|
||||
"style": "default",
|
||||
"tailwind": {
|
||||
"baseColor": "neutral",
|
||||
"config": "tailwind.config.js",
|
||||
"css": "src/global.css",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"tsx": true
|
||||
}
|
||||
|
||||
@@ -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/**"]
|
||||
}
|
||||
|
||||
@@ -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)://<config.siteUrl>/<path>
|
||||
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);
|
||||
}),
|
||||
|
||||
@@ -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 = {
|
||||
// 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",
|
||||
|
||||
// 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,
|
||||
|
||||
// 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",
|
||||
// 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,
|
||||
};
|
||||
|
||||
// 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 {
|
||||
|
||||
+24
-23
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
+76
-76
@@ -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",
|
||||
},
|
||||
{
|
||||
destination: "/team",
|
||||
permanent: false,
|
||||
source: "/create-api-key",
|
||||
destination: "/team",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
destination: "/team",
|
||||
permanent: false,
|
||||
source: "/dashboard/settings",
|
||||
destination: "/team",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
{
|
||||
destination: "/contracts/deployment-tool",
|
||||
permanent: false,
|
||||
source: "/deploy",
|
||||
destination: "/contracts/deployment-tool",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
destination: "/contracts/deployment-tool",
|
||||
permanent: false,
|
||||
source: "/publish",
|
||||
destination: "/contracts/deployment-tool",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/smart-contracts",
|
||||
destination: "/contracts/explore",
|
||||
permanent: false,
|
||||
source: "/smart-contracts",
|
||||
},
|
||||
{
|
||||
destination: "/sdk",
|
||||
permanent: false,
|
||||
source: "/ui-components",
|
||||
destination: "/sdk",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
destination: "/sdk",
|
||||
permanent: false,
|
||||
source: "/interact",
|
||||
destination: "/sdk",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
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/<slug> to /templates/<slug>
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -5,28 +5,32 @@
|
||||
import * as Sentry from "@sentry/nextjs";
|
||||
|
||||
Sentry.init({
|
||||
dsn: "https://[email protected]/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://[email protected]/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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -42,8 +42,8 @@ export async function acceptInvite(options: {
|
||||
} catch {}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
errorMessage,
|
||||
ok: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "PUT",
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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",
|
||||
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"),
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name:
|
||||
options?.name ?? `Your Projects ${format(new Date(), "MMM d yyyy")}`,
|
||||
slug: options?.slug ?? randomBytes(20).toString("hex"),
|
||||
billingEmail: null,
|
||||
image: null,
|
||||
}),
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -47,35 +47,35 @@ async function proxy<T>(
|
||||
}
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ export async function updateAccount(values: {
|
||||
}
|
||||
|
||||
const res = await fetch(`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/account`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(values),
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "PUT",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
insight: false,
|
||||
pay: false,
|
||||
inAppWallet: false,
|
||||
ecosystemWallet: false,
|
||||
inAppWallet: false,
|
||||
insight: false,
|
||||
nebula: false,
|
||||
pay: false,
|
||||
rpc: false,
|
||||
sdk: false,
|
||||
storage: false,
|
||||
} as ActiveStatus;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<SupportedWebhookChain
|
||||
const response = await fetch(
|
||||
`https://${THIRDWEB_INSIGHT_API_DOMAIN}/service/chains`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
"x-service-api-key": process.env.INSIGHT_SERVICE_API_KEY || "",
|
||||
},
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -41,17 +41,17 @@ export async function getUnreadNotifications(cursor?: string) {
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
status: "error",
|
||||
reason: "unknown",
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
} as const;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as NotificationsApiResponse;
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
data,
|
||||
status: "success",
|
||||
} as const;
|
||||
}
|
||||
|
||||
@@ -77,17 +77,17 @@ export async function getArchivedNotifications(cursor?: string) {
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
status: "error",
|
||||
reason: "unknown",
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
} as const;
|
||||
}
|
||||
|
||||
const data = (await response.json()) as NotificationsApiResponse;
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
data,
|
||||
status: "success",
|
||||
} as const;
|
||||
}
|
||||
|
||||
@@ -109,9 +109,9 @@ export async function getUnreadNotificationsCount() {
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
status: "error",
|
||||
reason: "unknown",
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
} as const;
|
||||
}
|
||||
const data = (await response.json()) as {
|
||||
@@ -120,8 +120,8 @@ export async function getUnreadNotificationsCount() {
|
||||
};
|
||||
};
|
||||
return {
|
||||
status: "success",
|
||||
data,
|
||||
status: "success",
|
||||
} as const;
|
||||
}
|
||||
|
||||
@@ -135,20 +135,20 @@ export async function markNotificationAsRead(notificationId?: string) {
|
||||
NEXT_PUBLIC_THIRDWEB_API_HOST,
|
||||
);
|
||||
const response = await fetch(url, {
|
||||
method: "PUT",
|
||||
// if notificationId is provided, mark it as read, otherwise mark all as read
|
||||
body: JSON.stringify(notificationId ? { notificationId } : {}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
// if notificationId is provided, mark it as read, otherwise mark all as read
|
||||
body: JSON.stringify(notificationId ? { notificationId } : {}),
|
||||
method: "PUT",
|
||||
});
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
status: "error",
|
||||
reason: "unknown",
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
} as const;
|
||||
}
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import "server-only";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
|
||||
export type Project = ProjectResponse;
|
||||
|
||||
@@ -3,8 +3,8 @@ import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
|
||||
const TeamAccountRole = {
|
||||
OWNER: "OWNER",
|
||||
MEMBER: "MEMBER",
|
||||
OWNER: "OWNER",
|
||||
} as const;
|
||||
|
||||
export type TeamAccountRole =
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import "server-only";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { API_SERVER_SECRET } from "@/constants/server-envs";
|
||||
import type { TeamResponse } from "@thirdweb-dev/service-utils";
|
||||
import { cookies } from "next/headers";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
import { API_SERVER_SECRET } from "@/constants/server-envs";
|
||||
import { getValidAccount } from "../../app/(app)/account/settings/getAccount";
|
||||
import { getAuthToken } from "../../app/(app)/api/lib/getAuthToken";
|
||||
import { LAST_USED_TEAM_ID } from "../../constants/cookies";
|
||||
|
||||
@@ -14,19 +14,16 @@ type Webhook = {
|
||||
version?: number; // TODO (UB) make this mandatory after migration
|
||||
};
|
||||
|
||||
export async function getWebhooks(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
}) {
|
||||
export async function getWebhooks(props: { clientId: string; teamId: string }) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -49,19 +46,19 @@ export async function createWebhook(props: {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/webhooks`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
url: props.url,
|
||||
label: props.label,
|
||||
version: props.version,
|
||||
secret: props.secret,
|
||||
url: props.url,
|
||||
version: props.version,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -81,13 +78,13 @@ export async function deleteWebhook(props: {
|
||||
const res = await fetch(
|
||||
`${UB_BASE_URL}/v1/developer/webhooks/${props.webhookId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
|
||||
@@ -106,19 +103,16 @@ export type Fee = {
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export async function getFees(props: {
|
||||
clientId: string;
|
||||
teamId: string;
|
||||
}) {
|
||||
export async function getFees(props: { clientId: string; teamId: string }) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -138,17 +132,17 @@ export async function updateFee(props: {
|
||||
}) {
|
||||
const authToken = await getAuthToken();
|
||||
const res = await fetch(`${UB_BASE_URL}/v1/developer/fees`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-team-id": props.teamId,
|
||||
"x-client-id": props.clientId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
feeRecipient: props.feeRecipient,
|
||||
feeBps: props.feeBps,
|
||||
feeRecipient: props.feeRecipient,
|
||||
}),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
},
|
||||
method: "PUT",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
@@ -225,13 +219,13 @@ export async function getPayments(props: {
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": props.clientId,
|
||||
"x-team-id": props.teamId,
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -18,15 +18,13 @@ type PaymentLink = {
|
||||
purchaseData: Record<string, unknown> | 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 {
|
||||
clientId: data.clientId,
|
||||
title: data.title,
|
||||
imageUrl: data.imageUrl,
|
||||
receiver: data.receiver,
|
||||
destinationToken: data.destinationToken,
|
||||
amount: data.amount ? BigInt(data.amount) : undefined,
|
||||
clientId: data.clientId,
|
||||
destinationToken: data.destinationToken,
|
||||
imageUrl: data.imageUrl,
|
||||
purchaseData: data.purchaseData,
|
||||
receiver: data.receiver,
|
||||
title: data.title,
|
||||
} as PaymentLink;
|
||||
}
|
||||
|
||||
@@ -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<string, string>,
|
||||
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<string, string>,
|
||||
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<string, string>,
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<ChakraProvider theme={chakraTheme}>
|
||||
{props.children}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -38,12 +38,12 @@ export function CheckoutButton(props: {
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={buildCheckoutUrl({
|
||||
teamSlug: props.teamSlug,
|
||||
sku: props.sku,
|
||||
teamSlug: props.teamSlug,
|
||||
})}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{props.children}
|
||||
</Link>
|
||||
@@ -59,8 +59,8 @@ function BillingWarning({ teamSlug }: { teamSlug: string }) {
|
||||
<p className="text-sm">
|
||||
You have outstanding invoices. Please{" "}
|
||||
<Link
|
||||
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
|
||||
className="font-medium text-amber-700 underline transition-colors hover:text-amber-900 dark:text-amber-400 dark:hover:text-amber-300"
|
||||
href={`/team/${teamSlug}/~/settings/invoices?status=open`}
|
||||
>
|
||||
pay them
|
||||
</Link>{" "}
|
||||
@@ -78,17 +78,17 @@ export function BillingPortalButton(props: {
|
||||
return (
|
||||
<Button
|
||||
{...props.buttonProps}
|
||||
asChild
|
||||
className={cn(props.buttonProps?.className, "gap-2")}
|
||||
disabled={props.buttonProps?.disabled}
|
||||
asChild
|
||||
onClick={async (e) => {
|
||||
props.buttonProps?.onClick?.(e);
|
||||
}}
|
||||
>
|
||||
<a
|
||||
href={buildBillingPortalUrl({ teamSlug: props.teamSlug })}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{props.children}
|
||||
</a>
|
||||
|
||||
@@ -8,8 +8,8 @@ import { Button } from "../../ui/button";
|
||||
import { GradientAvatar } from "./GradientAvatar";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Avatars/GradientAvatar",
|
||||
component: Story,
|
||||
title: "blocks/Avatars/GradientAvatar",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -26,64 +26,64 @@ function Story() {
|
||||
|
||||
<BadgeContainer label="No Src, No id - Skeleton">
|
||||
<GradientAvatar
|
||||
id={undefined}
|
||||
src={undefined}
|
||||
className="size-20"
|
||||
client={storybookThirdwebClient}
|
||||
id={undefined}
|
||||
src={undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Src, id=foo - Skeleton">
|
||||
<GradientAvatar
|
||||
id={"foo"}
|
||||
src={undefined}
|
||||
className="size-20"
|
||||
client={storybookThirdwebClient}
|
||||
id={"foo"}
|
||||
src={undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid Src, id=foo - Gradient">
|
||||
<GradientAvatar
|
||||
id={"foo"}
|
||||
src={""}
|
||||
className="size-20"
|
||||
client={storybookThirdwebClient}
|
||||
id={"foo"}
|
||||
src={""}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid Src, id=bar - Gradient">
|
||||
<GradientAvatar
|
||||
id={"bar"}
|
||||
src={""}
|
||||
className="size-20"
|
||||
client={storybookThirdwebClient}
|
||||
id={"bar"}
|
||||
src={""}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Empty/Invalid Src, No id - default fallback">
|
||||
<GradientAvatar
|
||||
src="invalid-src"
|
||||
id={undefined}
|
||||
className="size-20"
|
||||
client={storybookThirdwebClient}
|
||||
id={undefined}
|
||||
src="invalid-src"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Valid Src, https">
|
||||
<GradientAvatar
|
||||
src="https://picsum.photos/200/300"
|
||||
id={undefined}
|
||||
className="size-20"
|
||||
client={storybookThirdwebClient}
|
||||
id={undefined}
|
||||
src="https://picsum.photos/200/300"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Valid Src, ipfs">
|
||||
<GradientAvatar
|
||||
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
|
||||
id={undefined}
|
||||
className="size-20"
|
||||
client={storybookThirdwebClient}
|
||||
id={undefined}
|
||||
src="ipfs://QmZbeJYEs7kCJHyQxjxU2SJUtjSAr4m87wzJFJUyWomKdj/Smily.svg"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -100,7 +100,7 @@ function ToggleTest() {
|
||||
return (
|
||||
<div className="relative flex flex-col gap-10 border p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
setData(undefined);
|
||||
@@ -111,7 +111,7 @@ function ToggleTest() {
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
variant="outline"
|
||||
>
|
||||
Toggle Src
|
||||
</Button>
|
||||
@@ -120,19 +120,19 @@ function ToggleTest() {
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<GradientAvatar
|
||||
src={data?.src}
|
||||
id={data?.id}
|
||||
className="size-20"
|
||||
client={storybookThirdwebClient}
|
||||
id={data?.id}
|
||||
src={data?.src}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<GradientAvatar
|
||||
className="size-20"
|
||||
src={data ? "invalid-src" : undefined}
|
||||
id={undefined}
|
||||
client={storybookThirdwebClient}
|
||||
id={undefined}
|
||||
src={data ? "invalid-src" : undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<Img
|
||||
src={resolvedSrc}
|
||||
className={cn("rounded-full", props.className)}
|
||||
fallback={props.id ? <GradientBlobbie id={props.id} /> : undefined}
|
||||
src={resolvedSrc}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -27,17 +27,17 @@ function Story() {
|
||||
|
||||
<BadgeContainer label="No Src - Skeleton">
|
||||
<ProjectAvatar
|
||||
src={undefined}
|
||||
className="size-6"
|
||||
client={storybookThirdwebClient}
|
||||
src={undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src - BoxIcon Fallback">
|
||||
<ProjectAvatar
|
||||
src={""}
|
||||
className="size-6"
|
||||
client={storybookThirdwebClient}
|
||||
src={""}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -54,7 +54,7 @@ function ToggleTest() {
|
||||
return (
|
||||
<div className="relative flex flex-col gap-10 border p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
setData(undefined);
|
||||
@@ -65,7 +65,7 @@ function ToggleTest() {
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
variant="outline"
|
||||
>
|
||||
Toggle Src
|
||||
</Button>
|
||||
@@ -74,17 +74,17 @@ function ToggleTest() {
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<ProjectAvatar
|
||||
src={data?.src}
|
||||
className="size-6"
|
||||
client={storybookThirdwebClient}
|
||||
src={data?.src}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<ProjectAvatar
|
||||
src={data ? "invalid-src" : undefined}
|
||||
className="size-6"
|
||||
client={storybookThirdwebClient}
|
||||
src={data ? "invalid-src" : undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
@@ -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 (
|
||||
<Img
|
||||
src={
|
||||
resolveSchemeWithErrorHandler({
|
||||
uri: props.src,
|
||||
client: props.client,
|
||||
}) || ""
|
||||
}
|
||||
className={cn("rounded-full border border-border", props.className)}
|
||||
alt={""}
|
||||
className={cn("rounded-full border border-border", props.className)}
|
||||
fallback={
|
||||
<div className="flex items-center justify-center bg-card">
|
||||
<BoxIcon className="size-[50%] text-muted-foreground" />
|
||||
</div>
|
||||
}
|
||||
src={
|
||||
resolveSchemeWithErrorHandler({
|
||||
client: props.client,
|
||||
uri: props.src,
|
||||
}) || ""
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -24,29 +24,29 @@ function Story() {
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 py-10">
|
||||
<BadgeContainer label="Base">
|
||||
<DangerSettingCard
|
||||
title="This is a title"
|
||||
description="This is a description"
|
||||
buttonLabel="Some Action"
|
||||
buttonOnClick={() => {}}
|
||||
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"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Loading">
|
||||
<DangerSettingCard
|
||||
title="This is a title"
|
||||
description="This is a description"
|
||||
buttonLabel="Some Action"
|
||||
buttonOnClick={() => {}}
|
||||
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"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
|
||||
@@ -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: {
|
||||
)}
|
||||
>
|
||||
<Dialog
|
||||
open={isConfirmationDialogOpen}
|
||||
onOpenChange={(v) => {
|
||||
setIsConfirmationDialogOpen(v);
|
||||
if (!v) {
|
||||
props.confirmationDialog.onClose?.();
|
||||
}
|
||||
}}
|
||||
open={isConfirmationDialogOpen}
|
||||
>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="gap-2 bg-red-600 font-semibold text-white hover:bg-red-600/80"
|
||||
disabled={props.isDisabled || props.isPending}
|
||||
variant="destructive"
|
||||
>
|
||||
{props.isPending && <Spinner className="size-3" />}
|
||||
{props.buttonLabel}
|
||||
@@ -99,10 +99,10 @@ export function DangerSettingCard(props: {
|
||||
</DialogClose>
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="gap-2 bg-red-600 font-semibold text-white hover:bg-red-600/80"
|
||||
onClick={props.buttonOnClick}
|
||||
disabled={props.isPending}
|
||||
onClick={props.buttonOnClick}
|
||||
variant="destructive"
|
||||
>
|
||||
{props.isPending && <Spinner className="size-3" />}
|
||||
{props.buttonLabel}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { DownloadIcon } from "lucide-react";
|
||||
import Papa from "papaparse";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export function ExportToCSVButton(props: {
|
||||
@@ -34,12 +34,12 @@ export function ExportToCSVButton(props: {
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={props.disabled || exportMutation.isPending}
|
||||
className={cn("flex items-center gap-2 border text-xs", props.className)}
|
||||
disabled={props.disabled || exportMutation.isPending}
|
||||
onClick={async () => {
|
||||
exportMutation.mutate();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
{exportMutation.isPending ? (
|
||||
<>
|
||||
@@ -58,8 +58,8 @@ export function ExportToCSVButton(props: {
|
||||
|
||||
function convertToCSVFormat(data: { header: string[]; rows: string[][] }) {
|
||||
return Papa.unparse({
|
||||
fields: data.header,
|
||||
data: data.rows,
|
||||
fields: data.header,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { AsteriskIcon, InfoIcon } from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
|
||||
export function FormFieldSetup(props: {
|
||||
htmlFor?: string;
|
||||
|
||||
@@ -2,13 +2,13 @@ import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { ImageIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../stories/utils";
|
||||
import { Spinner } from "../ui/Spinner/Spinner";
|
||||
import { Button } from "../ui/button";
|
||||
import { Spinner } from "../ui/Spinner/Spinner";
|
||||
import { Img } from "./Img";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Img",
|
||||
component: Story,
|
||||
title: "blocks/Img",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -51,24 +51,24 @@ function Story() {
|
||||
|
||||
<BadgeContainer label="Custom Skeleton">
|
||||
<Img
|
||||
src={undefined}
|
||||
className="size-20"
|
||||
skeleton={
|
||||
<div className="flex items-center justify-center rounded-lg border">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
}
|
||||
className="size-20"
|
||||
src={undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Custom Fallback">
|
||||
<Img
|
||||
className="size-20"
|
||||
fallback={
|
||||
<div className="flex items-center justify-center rounded-lg border">
|
||||
<ImageIcon className="size-10 text-muted-foreground" />
|
||||
</div>
|
||||
}
|
||||
className="size-20"
|
||||
src="invalid-src"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
@@ -82,7 +82,7 @@ function ToggleTest() {
|
||||
return (
|
||||
<div className="relative flex flex-col gap-10 border p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
onClick={() => {
|
||||
if (src) {
|
||||
setSrc(undefined);
|
||||
@@ -90,7 +90,7 @@ function ToggleTest() {
|
||||
setSrc("https://picsum.photos/400");
|
||||
}
|
||||
}}
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
variant="outline"
|
||||
>
|
||||
Toggle Src
|
||||
</Button>
|
||||
|
||||
@@ -53,30 +53,30 @@ export function Img(props: imgElementProps) {
|
||||
<img
|
||||
{...restProps}
|
||||
// avoid setting empty src string to prevent request to the entire page
|
||||
src={restProps.src || undefined}
|
||||
ref={imgRef}
|
||||
onError={() => {
|
||||
setStatus("fallback");
|
||||
}}
|
||||
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"
|
||||
onError={() => {
|
||||
setStatus("fallback");
|
||||
}}
|
||||
ref={imgRef}
|
||||
src={restProps.src || undefined}
|
||||
style={{
|
||||
opacity: status === "loaded" ? 1 : 0,
|
||||
...restProps.style,
|
||||
}}
|
||||
/>
|
||||
|
||||
{status !== "loaded" && (
|
||||
<div
|
||||
style={restProps.style}
|
||||
className={cn(
|
||||
"fade-in-0 absolute inset-0 overflow-hidden transition-opacity duration-300 [&>*]:h-full [&>*]:w-full",
|
||||
className,
|
||||
)}
|
||||
style={restProps.style}
|
||||
>
|
||||
{status === "pending" && (skeleton || defaultSkeleton)}
|
||||
{status === "fallback" && (fallback || defaultFallback)}
|
||||
|
||||
@@ -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 (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<Dialog onOpenChange={setIsOpen} open={isOpen}>
|
||||
<DialogTrigger asChild>{props.trigger || defaultTrigger}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="no-scrollbar max-h-[80vh] overflow-auto rounded-t-xl rounded-b-none p-4"
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
import { MultiNetworkSelector } from "./NetworkSelectors";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Cards/MultiNetworkSelector",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/Cards/MultiNetworkSelector",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
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<number[]>(props.selectedChainIds);
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
<MultiNetworkSelector
|
||||
selectedChainIds={chainIds}
|
||||
client={storybookThirdwebClient}
|
||||
onChange={setChainIds}
|
||||
selectedChainIds={chainIds}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
|
||||
@@ -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: {
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIconClient
|
||||
className="size-5"
|
||||
client={props.client}
|
||||
loading="lazy"
|
||||
src={chain.icon?.url}
|
||||
client={props.client}
|
||||
/>
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2">
|
||||
<Badge className="gap-2" variant="outline">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
@@ -119,24 +119,24 @@ export function MultiNetworkSelector(props: {
|
||||
|
||||
return (
|
||||
<MultiSelect
|
||||
searchPlaceholder="Search by Name or Chain Id"
|
||||
selectedValues={props.selectedChainIds.map(String)}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
customTrigger={props.customTrigger}
|
||||
options={options}
|
||||
align={props.align}
|
||||
side={props.side}
|
||||
showSelectedValuesInModal={props.showSelectedValuesInModal}
|
||||
className={props.className}
|
||||
customTrigger={props.customTrigger}
|
||||
disabled={allChains.length === 0}
|
||||
onSelectedValuesChange={(chainIds) => {
|
||||
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: {
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<ChainIconClient
|
||||
className="size-5"
|
||||
src={chain.icon?.url}
|
||||
client={props.client}
|
||||
loading="lazy"
|
||||
src={chain.icon?.url}
|
||||
/>
|
||||
{cleanChainName(chain.name)}
|
||||
</span>
|
||||
|
||||
{!props.disableChainId && (
|
||||
<Badge variant="outline" className="gap-2 max-sm:hidden">
|
||||
<Badge className="gap-2 max-sm:hidden" variant="outline">
|
||||
<span className="text-muted-foreground">Chain ID</span>
|
||||
{chain.chainId}
|
||||
</Badge>
|
||||
@@ -231,26 +231,26 @@ export function SingleNetworkSelector(props: {
|
||||
|
||||
return (
|
||||
<SelectWithSearch
|
||||
searchPlaceholder="Search by Name or Chain ID"
|
||||
value={String(props.chainId)}
|
||||
showCheck={false}
|
||||
options={options}
|
||||
align={props.align}
|
||||
className={props.className}
|
||||
closeOnSelect={true}
|
||||
disabled={isLoadingChains}
|
||||
onValueChange={(chainId) => {
|
||||
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)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function RouteDiscoveryCard(
|
||||
props: React.PropsWithChildren<{
|
||||
@@ -66,12 +66,12 @@ export function RouteDiscoveryCard(
|
||||
|
||||
{props.saveButton && !props.noPermissionText && (
|
||||
<Button
|
||||
size="sm"
|
||||
className={cn("gap-2", props.saveButton.className)}
|
||||
onClick={props.saveButton.onClick}
|
||||
disabled={props.saveButton.disabled || props.saveButton.isPending}
|
||||
variant={props.saveButton.variant || "outline"}
|
||||
onClick={props.saveButton.onClick}
|
||||
size="sm"
|
||||
type={props.saveButton.type}
|
||||
variant={props.saveButton.variant || "outline"}
|
||||
>
|
||||
{props.saveButton.isPending && <Spinner className="size-3" />}
|
||||
{props.saveButton.label ||
|
||||
|
||||
@@ -3,13 +3,13 @@ import { BadgeContainer } from "../../../stories/utils";
|
||||
import { SettingsCard } from "./SettingsCard";
|
||||
|
||||
const meta = {
|
||||
title: "Blocks/Cards/SettingsCard",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "Blocks/Cards/SettingsCard",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -25,8 +25,8 @@ function Story() {
|
||||
<BadgeContainer label="No Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={undefined}
|
||||
errorText={undefined}
|
||||
header={undefined}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
@@ -37,11 +37,11 @@ function Story() {
|
||||
<BadgeContainer label="Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText={undefined}
|
||||
header={{
|
||||
description: "This is some description for this card",
|
||||
title: "Some Title",
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
@@ -52,11 +52,11 @@ function Story() {
|
||||
<BadgeContainer label="Error">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
}}
|
||||
errorText="This is error text"
|
||||
header={{
|
||||
description: "This is some description for this card",
|
||||
title: "Some Title",
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
saveButton={undefined}
|
||||
>
|
||||
@@ -67,17 +67,17 @@ function Story() {
|
||||
<BadgeContainer label="No Permission">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
errorText={undefined}
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
title: "Some Title",
|
||||
}}
|
||||
noPermissionText="You do not have permission to edit this"
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText="You do not have permission to edit this"
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
@@ -86,17 +86,17 @@ function Story() {
|
||||
<BadgeContainer label="Save">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
errorText={undefined}
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
title: "Some Title",
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
@@ -105,17 +105,17 @@ function Story() {
|
||||
<BadgeContainer label="Save Disabled">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
errorText={undefined}
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
title: "Some Title",
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
saveButton={{
|
||||
disabled: true,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
@@ -124,17 +124,17 @@ function Story() {
|
||||
<BadgeContainer label="Save Loading">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
errorText={undefined}
|
||||
header={{
|
||||
title: "Some Title",
|
||||
description: "This is some description for this card",
|
||||
title: "Some Title",
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isPending: true,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
</SettingsCard>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function SettingsCard(
|
||||
props: React.PropsWithChildren<{
|
||||
@@ -66,12 +66,12 @@ export function SettingsCard(
|
||||
|
||||
{props.saveButton && !props.noPermissionText && (
|
||||
<Button
|
||||
size="sm"
|
||||
className={cn("gap-2", props.saveButton.className)}
|
||||
onClick={props.saveButton.onClick}
|
||||
disabled={props.saveButton.disabled || props.saveButton.isPending}
|
||||
variant={props.saveButton.variant || "outline"}
|
||||
onClick={props.saveButton.onClick}
|
||||
size="sm"
|
||||
type={props.saveButton.type}
|
||||
variant={props.saveButton.variant || "outline"}
|
||||
>
|
||||
{props.saveButton.isPending && <Spinner className="size-3" />}
|
||||
{props.saveButton.label ||
|
||||
|
||||
@@ -65,12 +65,12 @@ export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
const isExternal = link.href.startsWith("http");
|
||||
return (
|
||||
<NavLink
|
||||
activeClassName="text-foreground bg-accent"
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
href={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-accent"
|
||||
activeClassName="text-foreground bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
>
|
||||
{link.icon && <link.icon className="size-4" />}
|
||||
{link.label}
|
||||
|
||||
@@ -2,13 +2,13 @@ import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { SidebarLayout } from "./SidebarLayout";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/SidebarLayout",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/SidebarLayout",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -35,8 +35,8 @@ function Story() {
|
||||
label: "eiusmod",
|
||||
},
|
||||
{
|
||||
label: "incididunt",
|
||||
href: "https://playground.thirdweb.com/",
|
||||
label: "incididunt",
|
||||
},
|
||||
]}
|
||||
>
|
||||
|
||||
@@ -15,9 +15,9 @@ import {
|
||||
import { cn } from "../../lib/utils";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
import { Separator } from "../ui/separator";
|
||||
import { AppFooter } from "./app-footer";
|
||||
import { MobileSidebar, useActiveSidebarLink } from "./MobileSidebar";
|
||||
import { CustomSidebar, type SidebarLink } from "./Sidebar";
|
||||
import { AppFooter } from "./app-footer";
|
||||
|
||||
export function SidebarLayout(props: {
|
||||
sidebarLinks: SidebarLink[];
|
||||
@@ -35,8 +35,8 @@ export function SidebarLayout(props: {
|
||||
)}
|
||||
>
|
||||
<CustomSidebar
|
||||
links={sidebarLinks}
|
||||
className={props.desktopSidebarClassName}
|
||||
links={sidebarLinks}
|
||||
/>
|
||||
<MobileSidebar
|
||||
links={sidebarLinks}
|
||||
@@ -65,13 +65,13 @@ export function FullWidthSidebarLayout(props: {
|
||||
)}
|
||||
>
|
||||
{/* left - sidebar */}
|
||||
<Sidebar collapsible="icon" side="left" className="pt-2">
|
||||
<Sidebar className="pt-2" collapsible="icon" side="left">
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupContent>
|
||||
<RenderSidebarGroup
|
||||
sidebarLinks={contentSidebarLinks}
|
||||
groupName={undefined}
|
||||
sidebarLinks={contentSidebarLinks}
|
||||
/>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
@@ -80,8 +80,8 @@ export function FullWidthSidebarLayout(props: {
|
||||
{footerSidebarLinks && (
|
||||
<SidebarFooter className="pb-3">
|
||||
<RenderSidebarGroup
|
||||
sidebarLinks={footerSidebarLinks}
|
||||
groupName={undefined}
|
||||
sidebarLinks={footerSidebarLinks}
|
||||
/>
|
||||
</SidebarFooter>
|
||||
)}
|
||||
@@ -119,10 +119,10 @@ function RenderSidebarGroup(props: {
|
||||
<SidebarMenuItem key={link.href}>
|
||||
<SidebarMenuButton asChild>
|
||||
<NavLink
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
activeClassName="text-foreground bg-accent"
|
||||
className="flex items-center gap-2 text-muted-foreground text-sm hover:bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
href={link.href}
|
||||
onClick={() => {
|
||||
sidebar.setOpenMobile(false);
|
||||
}}
|
||||
@@ -136,14 +136,14 @@ function RenderSidebarGroup(props: {
|
||||
}
|
||||
|
||||
if ("separator" in link) {
|
||||
return <SidebarSeparator key={Math.random()} className="my-1" />;
|
||||
return <SidebarSeparator className="my-1" key={Math.random()} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderSidebarGroup
|
||||
sidebarLinks={link.links}
|
||||
groupName={link.group}
|
||||
key={link.group}
|
||||
sidebarLinks={link.links}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -157,7 +157,7 @@ function MobileSidebarTrigger(props: { links: SidebarLink[] }) {
|
||||
return (
|
||||
<div className="flex items-center gap-3 border-b px-4 py-4 lg:hidden">
|
||||
<SidebarTrigger className="size-4" />
|
||||
<Separator orientation="vertical" className="h-4" />
|
||||
<Separator className="h-4" orientation="vertical" />
|
||||
{activeLink && <span className="text-sm">{activeLink.label}</span>}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { MultiSelect } from "./multi-select";
|
||||
|
||||
interface SignatureOption {
|
||||
@@ -85,38 +85,38 @@ export function SignatureSelector({
|
||||
// Custom render for MultiSelect's search input
|
||||
const customSearchInput = (
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
autoComplete="off"
|
||||
className={cn(
|
||||
"w-full border-0 border-border border-b bg-transparent py-4 pr-2 pl-10 text-sm focus-visible:ring-0 focus-visible:ring-offset-0",
|
||||
disabled && "cursor-not-allowed opacity-50",
|
||||
)}
|
||||
placeholder={placeholder}
|
||||
value={searchValue}
|
||||
disabled={disabled}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
disabled={disabled}
|
||||
autoComplete="off"
|
||||
placeholder={placeholder}
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={searchValue}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<MultiSelect
|
||||
options={allOptions}
|
||||
selectedValues={value ? [value] : []}
|
||||
onSelectedValuesChange={handleSelectedValuesChange}
|
||||
placeholder={placeholder}
|
||||
maxCount={1}
|
||||
disabled={disabled}
|
||||
searchPlaceholder={placeholder}
|
||||
customSearchInput={customSearchInput}
|
||||
customTrigger={null}
|
||||
renderOption={(option) => <span>{option.label}</span>}
|
||||
disabled={disabled}
|
||||
maxCount={1}
|
||||
onSelectedValuesChange={handleSelectedValuesChange}
|
||||
options={allOptions}
|
||||
overrideSearchFn={(option, searchTerm) =>
|
||||
option.label.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
option.value.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
}
|
||||
customSearchInput={customSearchInput}
|
||||
placeholder={placeholder}
|
||||
renderOption={(option) => <span>{option.label}</span>}
|
||||
searchPlaceholder={placeholder}
|
||||
selectedValues={value ? [value] : []}
|
||||
/>
|
||||
{isCustomValue && (
|
||||
<div className="mt-2 rounded border border-warning-200 bg-warning-50 px-2 py-1 text-warning-700 text-xs">
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
import { SingleNetworkSelector } from "./NetworkSelectors";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Cards/SingleNetworkSelector",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/Cards/SingleNetworkSelector",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -26,12 +26,12 @@ export const Variants: Story = {
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-6xl flex-col gap-8 py-10">
|
||||
<Variant label="No Chain ID selected by default" chainId={undefined} />
|
||||
<Variant label="Polygon selected by default" chainId={137} />
|
||||
<Variant chainId={undefined} label="No Chain ID selected by default" />
|
||||
<Variant chainId={137} label="Polygon selected by default" />
|
||||
<Variant
|
||||
label="Show certain chains only"
|
||||
chainId={undefined}
|
||||
chainIds={[1, 137, 10]}
|
||||
label="Show certain chains only"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -46,10 +46,10 @@ function Variant(props: {
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
<SingleNetworkSelector
|
||||
client={storybookThirdwebClient}
|
||||
chainId={chainId}
|
||||
onChange={setChainId}
|
||||
chainIds={props.chainIds}
|
||||
client={storybookThirdwebClient}
|
||||
onChange={setChainId}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
|
||||
@@ -7,13 +7,13 @@ import {
|
||||
import { TokenSelector } from "./TokenSelector";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Cards/TokenSelector",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/Cards/TokenSelector",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -31,10 +31,7 @@ function Story() {
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
label: string;
|
||||
selectedChainId?: number;
|
||||
}) {
|
||||
function Variant(props: { label: string; selectedChainId?: number }) {
|
||||
const [token, setToken] = useState<
|
||||
| {
|
||||
address: string;
|
||||
@@ -47,8 +44,6 @@ function Variant(props: {
|
||||
<BadgeContainer label={props.label}>
|
||||
<TokenSelector
|
||||
addNativeTokenIfMissing={false}
|
||||
showCheck={false}
|
||||
selectedToken={token}
|
||||
chainId={props.selectedChainId}
|
||||
client={storybookThirdwebClient}
|
||||
onChange={(v) => {
|
||||
@@ -57,6 +52,8 @@ function Variant(props: {
|
||||
chainId: v.chainId,
|
||||
});
|
||||
}}
|
||||
selectedToken={token}
|
||||
showCheck={false}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
|
||||
@@ -2,9 +2,9 @@ import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useTokensData } from "hooks/tokens/tokens";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import {
|
||||
getAddress,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
type ThirdwebClient,
|
||||
getAddress,
|
||||
} from "thirdweb";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import { replaceIpfsUrl } from "../../../lib/sdk";
|
||||
@@ -55,14 +55,14 @@ export function TokenSelector(props: {
|
||||
if (!hasNativeToken && props.chainId) {
|
||||
return [
|
||||
{
|
||||
address: checksummedNativeTokenAddress,
|
||||
chainId: props.chainId,
|
||||
decimals: 18,
|
||||
name:
|
||||
idToChain.get(props.chainId)?.nativeCurrency.name ??
|
||||
"Native Token",
|
||||
symbol:
|
||||
idToChain.get(props.chainId)?.nativeCurrency.symbol ?? "ETH",
|
||||
decimals: 18,
|
||||
chainId: props.chainId,
|
||||
address: checksummedNativeTokenAddress,
|
||||
} satisfies TokenMetadata,
|
||||
...tokensQuery.data,
|
||||
];
|
||||
@@ -129,22 +129,22 @@ export function TokenSelector(props: {
|
||||
<span className="flex grow gap-2 truncate text-left">
|
||||
<Img
|
||||
// render different image element if src changes to avoid showing old image while loading new one
|
||||
key={resolvedSrc}
|
||||
className={cn("size-5 rounded-full object-contain")}
|
||||
src={resolvedSrc}
|
||||
loading={"lazy"}
|
||||
alt=""
|
||||
className={cn("size-5 rounded-full object-contain")}
|
||||
fallback={<img alt="" src={fallbackChainIcon} />}
|
||||
key={resolvedSrc}
|
||||
loading={"lazy"}
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
fallback={<img src={fallbackChainIcon} alt="" />}
|
||||
skeleton={
|
||||
<div className="animate-pulse rounded-full bg-border" />
|
||||
}
|
||||
src={resolvedSrc}
|
||||
/>
|
||||
{token.symbol}
|
||||
</span>
|
||||
|
||||
{!props.disableAddress && (
|
||||
<Badge variant="outline" className="gap-2 py-1 max-sm:hidden">
|
||||
<Badge className="gap-2 py-1 max-sm:hidden" variant="outline">
|
||||
<span className="text-muted-foreground">Address</span>
|
||||
{shortenAddress(token.address, 4)}
|
||||
</Badge>
|
||||
@@ -172,9 +172,10 @@ export function TokenSelector(props: {
|
||||
|
||||
return (
|
||||
<SelectWithSearch
|
||||
searchPlaceholder="Search by name or symbol"
|
||||
value={selectedValue}
|
||||
options={options}
|
||||
align={props.align}
|
||||
className={props.className}
|
||||
closeOnSelect={true}
|
||||
disabled={tokensQuery.isPending || props.disabled}
|
||||
onValueChange={(tokenAddress) => {
|
||||
const token = addressChainToToken.get(tokenAddress);
|
||||
if (!token) {
|
||||
@@ -182,20 +183,19 @@ export function TokenSelector(props: {
|
||||
}
|
||||
props.onChange(token);
|
||||
}}
|
||||
closeOnSelect={true}
|
||||
showCheck={props.showCheck}
|
||||
options={options}
|
||||
overrideSearchFn={searchFn}
|
||||
placeholder={
|
||||
tokensQuery.isPending
|
||||
? "Loading Tokens"
|
||||
: props.placeholder || "Select Token"
|
||||
}
|
||||
overrideSearchFn={searchFn}
|
||||
renderOption={renderOption}
|
||||
className={props.className}
|
||||
popoverContentClassName={props.popoverContentClassName}
|
||||
disabled={tokensQuery.isPending || props.disabled}
|
||||
renderOption={renderOption}
|
||||
searchPlaceholder="Search by name or symbol"
|
||||
showCheck={props.showCheck}
|
||||
side={props.side}
|
||||
align={props.align}
|
||||
value={selectedValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,48 +8,48 @@ function Story() {
|
||||
<div className="container flex max-w-4xl flex-col gap-8 py-10">
|
||||
<BadgeContainer label="Green with icon (default)">
|
||||
<UpsellBannerCard
|
||||
title="Unlock more with thirdweb"
|
||||
description="Upgrade to increase limits and access advanced features."
|
||||
accentColor="green"
|
||||
cta={{
|
||||
text: "View plans",
|
||||
icon: <ArrowRightIcon className="size-4" />,
|
||||
link: "#",
|
||||
text: "View plans",
|
||||
}}
|
||||
description="Upgrade to increase limits and access advanced features."
|
||||
icon={<RocketIcon className="size-5" />}
|
||||
title="Unlock more with thirdweb"
|
||||
trackingCategory="storybook"
|
||||
trackingLabel="green"
|
||||
icon={<RocketIcon className="size-5" />}
|
||||
accentColor="green"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Blue accent">
|
||||
<UpsellBannerCard
|
||||
title="Need more storage?"
|
||||
description="Add additional space to your account."
|
||||
accentColor="blue"
|
||||
cta={{
|
||||
text: "Upgrade storage",
|
||||
icon: <ArrowRightIcon className="size-4" />,
|
||||
link: "#",
|
||||
text: "Upgrade storage",
|
||||
}}
|
||||
description="Add additional space to your account."
|
||||
icon={<StarIcon className="size-5" />}
|
||||
title="Need more storage?"
|
||||
trackingCategory="storybook"
|
||||
trackingLabel="blue"
|
||||
icon={<StarIcon className="size-5" />}
|
||||
accentColor="blue"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Purple without leading icon">
|
||||
<UpsellBannerCard
|
||||
title="Join the beta"
|
||||
description="Get early access to experimental features."
|
||||
accentColor="purple"
|
||||
cta={{
|
||||
text: "Request access",
|
||||
icon: <ArrowRightIcon className="size-4" />,
|
||||
link: "#",
|
||||
text: "Request access",
|
||||
}}
|
||||
description="Get early access to experimental features."
|
||||
title="Join the beta"
|
||||
trackingCategory="storybook"
|
||||
trackingLabel="purple"
|
||||
accentColor="purple"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
@@ -57,13 +57,13 @@ function Story() {
|
||||
}
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Banners/UpsellBannerCard",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/Banners/UpsellBannerCard",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
|
||||
@@ -1,37 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const ACCENT = {
|
||||
green: {
|
||||
border: "border-green-600 dark:border-green-700",
|
||||
bgFrom: "from-green-50 dark:from-green-900/20",
|
||||
blur: "bg-green-600",
|
||||
title: "text-green-900 dark:text-green-200",
|
||||
desc: "text-green-800 dark:text-green-300",
|
||||
iconBg: "bg-green-600 text-white",
|
||||
btn: "bg-green-600 text-white hover:bg-green-700",
|
||||
},
|
||||
blue: {
|
||||
border: "border-blue-600 dark:border-blue-700",
|
||||
bgFrom: "from-blue-50 dark:from-blue-900/20",
|
||||
blur: "bg-blue-600",
|
||||
title: "text-blue-900 dark:text-blue-200",
|
||||
border: "border-blue-600 dark:border-blue-700",
|
||||
btn: "bg-blue-600 text-white hover:bg-blue-700",
|
||||
desc: "text-blue-800 dark:text-blue-300",
|
||||
iconBg: "bg-blue-600 text-white",
|
||||
btn: "bg-blue-600 text-white hover:bg-blue-700",
|
||||
title: "text-blue-900 dark:text-blue-200",
|
||||
},
|
||||
green: {
|
||||
bgFrom: "from-green-50 dark:from-green-900/20",
|
||||
blur: "bg-green-600",
|
||||
border: "border-green-600 dark:border-green-700",
|
||||
btn: "bg-green-600 text-white hover:bg-green-700",
|
||||
desc: "text-green-800 dark:text-green-300",
|
||||
iconBg: "bg-green-600 text-white",
|
||||
title: "text-green-900 dark:text-green-200",
|
||||
},
|
||||
purple: {
|
||||
border: "border-purple-600 dark:border-purple-700",
|
||||
bgFrom: "from-purple-50 dark:from-purple-900/20",
|
||||
blur: "bg-purple-600",
|
||||
title: "text-purple-900 dark:text-purple-200",
|
||||
border: "border-purple-600 dark:border-purple-700",
|
||||
btn: "bg-purple-600 text-white hover:bg-purple-700",
|
||||
desc: "text-purple-800 dark:text-purple-300",
|
||||
iconBg: "bg-purple-600 text-white",
|
||||
btn: "bg-purple-600 text-white hover:bg-purple-700",
|
||||
title: "text-purple-900 dark:text-purple-200",
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -97,18 +97,18 @@ export function UpsellBannerCard(props: UpsellBannerCardProps) {
|
||||
|
||||
<Button
|
||||
asChild
|
||||
size="sm"
|
||||
className={cn(
|
||||
"mt-2 gap-2 hover:translate-y-0 hover:shadow-inner sm:mt-0",
|
||||
color.btn,
|
||||
)}
|
||||
size="sm"
|
||||
>
|
||||
<Link
|
||||
href={props.cta.link}
|
||||
target={props.cta.target}
|
||||
rel={
|
||||
props.cta.target === "_blank" ? "noopener noreferrer" : undefined
|
||||
}
|
||||
target={props.cta.target}
|
||||
>
|
||||
{props.cta.text}
|
||||
{props.cta.icon && <span className="ml-2">{props.cta.icon}</span>}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "app/(app)/components/ThirdwebMiniLogo";
|
||||
import { GithubIcon } from "components/icons/brand-icons/GithubIcon";
|
||||
import { InstagramIcon } from "components/icons/brand-icons/InstagramIcon";
|
||||
@@ -9,6 +7,8 @@ import { TiktokIcon } from "components/icons/brand-icons/TiktokIcon";
|
||||
import { XIcon } from "components/icons/brand-icons/XIcon";
|
||||
import { YoutubeIcon } from "components/icons/brand-icons/YoutubeIcon";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type AppFooterProps = {
|
||||
className?: string;
|
||||
@@ -69,34 +69,34 @@ export function AppFooter(props: AppFooterProps) {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-row gap-3">
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Button asChild className="size-9" size="icon" variant="ghost">
|
||||
<Link
|
||||
href="https://github.com/thirdweb-dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<GithubIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Button asChild className="size-9" size="icon" variant="ghost">
|
||||
<Link
|
||||
href="https://www.tiktok.com/@thirdweb"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<TiktokIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Button asChild className="size-9" size="icon" variant="ghost">
|
||||
<Link
|
||||
href="https://www.instagram.com/thirdweb/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<InstagramIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Button asChild className="size-9" size="icon" variant="ghost">
|
||||
<Link
|
||||
href="hhttps://www.linkedin.com/company/third-web/"
|
||||
target="_blank"
|
||||
@@ -104,29 +104,29 @@ export function AppFooter(props: AppFooterProps) {
|
||||
<LinkedInIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Button asChild className="size-9" size="icon" variant="ghost">
|
||||
<Link
|
||||
href="https://www.youtube.com/@thirdweb_"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<YoutubeIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Button asChild className="size-9" size="icon" variant="ghost">
|
||||
<Link
|
||||
href="https://www.reddit.com/r/thirdweb/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<RedditIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Button asChild className="size-9" size="icon" variant="ghost">
|
||||
<Link
|
||||
href="https://x.com/thirdweb"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<XIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
@@ -151,9 +151,9 @@ function FooterLink(props: {
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
className="px-0 py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href={props.href}
|
||||
prefetch={false}
|
||||
className="px-0 py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
target="_blank"
|
||||
>
|
||||
{props.label}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "components/analytics/empty-chart-state";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -16,13 +23,6 @@ import {
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "components/analytics/empty-chart-state";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
|
||||
|
||||
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
header?: {
|
||||
@@ -77,7 +77,7 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
<CardContent
|
||||
className={cn(!props.header && "pt-6", props.cardContentClassName)}
|
||||
>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
<ChartContainer className={props.chartClassName} config={props.config}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
@@ -87,21 +87,20 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
) : (
|
||||
<AreaChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
{props.yAxis && <YAxis tickLine={false} axisLine={false} />}
|
||||
{props.yAxis && <YAxis axisLine={false} tickLine={false} />}
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={20}
|
||||
dataKey="time"
|
||||
tickFormatter={(value) =>
|
||||
format(
|
||||
new Date(value),
|
||||
props.xAxis?.sameDay ? "MMM dd, HH:mm" : "MMM dd",
|
||||
)
|
||||
}
|
||||
tickLine={false}
|
||||
tickMargin={20}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={true}
|
||||
content={
|
||||
<ChartTooltipContent
|
||||
hideLabel={
|
||||
@@ -111,15 +110,16 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
valueFormatter={props.toolTipValueFormatter}
|
||||
/>
|
||||
}
|
||||
cursor={true}
|
||||
/>
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`fill_${key}`}
|
||||
key={key}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y1="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
@@ -138,23 +138,23 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
{configKeys.map((key) =>
|
||||
key === "maxLine" ? (
|
||||
<Area
|
||||
key={key}
|
||||
type="monotone"
|
||||
dataKey="maxLine"
|
||||
stroke="#ef4444"
|
||||
strokeWidth={2}
|
||||
strokeDasharray="5 5"
|
||||
fill="none"
|
||||
key={key}
|
||||
stroke="#ef4444"
|
||||
strokeDasharray="5 5"
|
||||
strokeWidth={2}
|
||||
type="monotone"
|
||||
/>
|
||||
) : (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
key={key}
|
||||
stackId={props.variant !== "stacked" ? undefined : "a"}
|
||||
stroke={`var(--color-${key})`}
|
||||
type="natural"
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "components/analytics/empty-chart-state";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
import { Bar, BarChart, CartesianGrid, XAxis } from "recharts";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -18,12 +23,6 @@ import {
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
EmptyChartState,
|
||||
LoadingChartState,
|
||||
} from "components/analytics/empty-chart-state";
|
||||
import { format } from "date-fns";
|
||||
import { useMemo } from "react";
|
||||
|
||||
type ThirdwebBarChartProps<TConfig extends ChartConfig> = {
|
||||
// metadata
|
||||
@@ -71,7 +70,7 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
{props.customHeader && props.customHeader}
|
||||
|
||||
<CardContent className={cn(!props.header && "pt-6")}>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
<ChartContainer className={props.chartClassName} config={props.config}>
|
||||
{props.isPending ? (
|
||||
<LoadingChartState />
|
||||
) : props.data.length === 0 ? (
|
||||
@@ -82,11 +81,11 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
<BarChart accessibilityLayer data={props.data}>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={10}
|
||||
dataKey="time"
|
||||
tickFormatter={(value) => format(new Date(value), "MMM d")}
|
||||
tickLine={false}
|
||||
tickMargin={10}
|
||||
/>
|
||||
<ChartTooltip
|
||||
content={
|
||||
@@ -106,15 +105,15 @@ export function ThirdwebBarChart<TConfig extends ChartConfig>(
|
||||
)}
|
||||
{configKeys.map((key) => (
|
||||
<Bar
|
||||
key={key}
|
||||
className="stroke-background"
|
||||
dataKey={key}
|
||||
// if stacked then they should all be the same stackId
|
||||
// if grouped then they should all be unique stackId (so the key works great)
|
||||
stackId={variant === "stacked" ? "a" : key}
|
||||
fill={props.config[key]?.color}
|
||||
key={key}
|
||||
radius={[4, 4, 4, 4]}
|
||||
stackId={variant === "stacked" ? "a" : key}
|
||||
strokeWidth={1}
|
||||
className="stroke-background"
|
||||
/>
|
||||
))}
|
||||
</BarChart>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type ReactNode, useEffect, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ClientOnlyProps {
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import type React from "react";
|
||||
import { type Dispatch, type SetStateAction, useMemo } from "react";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { TabButtons } from "../ui/tabs";
|
||||
|
||||
export type CodeEnvironment =
|
||||
@@ -91,38 +91,36 @@ export const CodeSegment: React.FC<CodeSegmentProps> = ({
|
||||
>
|
||||
{!hideTabs && (
|
||||
<TabButtons
|
||||
tabs={environments.map((env) => ({
|
||||
label: env.title,
|
||||
onClick: () => setEnvironment(env.environment),
|
||||
isActive: activeEnvironment === env.environment,
|
||||
name: env.title,
|
||||
}))}
|
||||
tabClassName="text-sm gap-2 !text-sm"
|
||||
tabIconClassName="size-4"
|
||||
tabContainerClassName="px-3 pt-1.5 gap-0.5"
|
||||
hideBottomLine={!!onlyTabs}
|
||||
tabClassName="text-sm gap-2 !text-sm"
|
||||
tabContainerClassName="px-3 pt-1.5 gap-0.5"
|
||||
tabIconClassName="size-4"
|
||||
tabs={environments.map((env) => ({
|
||||
isActive: activeEnvironment === env.environment,
|
||||
label: env.title,
|
||||
name: env.title,
|
||||
onClick: () => setEnvironment(env.environment),
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
|
||||
{onlyTabs ? null : (
|
||||
<>
|
||||
<CodeClient
|
||||
code={code}
|
||||
className="rounded-none border-none"
|
||||
lang={
|
||||
isInstallCommand
|
||||
? activeEnvironment === "react-native"
|
||||
? "tsx"
|
||||
: "bash"
|
||||
: activeEnvironment === "react" ||
|
||||
activeEnvironment === "react-native"
|
||||
? "tsx"
|
||||
: activeEnvironment === "unity"
|
||||
? "cpp"
|
||||
: activeEnvironment
|
||||
}
|
||||
/>
|
||||
</>
|
||||
<CodeClient
|
||||
className="rounded-none border-none"
|
||||
code={code}
|
||||
lang={
|
||||
isInstallCommand
|
||||
? activeEnvironment === "react-native"
|
||||
? "tsx"
|
||||
: "bash"
|
||||
: activeEnvironment === "react" ||
|
||||
activeEnvironment === "react-native"
|
||||
? "tsx"
|
||||
: activeEnvironment === "unity"
|
||||
? "cpp"
|
||||
: activeEnvironment
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -37,10 +37,10 @@ function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-[700px] flex-col gap-10 py-10">
|
||||
<Select
|
||||
value={mode}
|
||||
onValueChange={(v) => {
|
||||
setMode(v as Mode);
|
||||
}}
|
||||
value={mode}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue />
|
||||
@@ -58,12 +58,12 @@ function Story() {
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Variant envsToShow={["javascript"]} storyLabel="1 env" mode={mode} />
|
||||
<Variant envsToShow={["javascript"]} mode={mode} storyLabel="1 env" />
|
||||
|
||||
<Variant
|
||||
envsToShow={["javascript", "typescript"]}
|
||||
storyLabel="2 envs"
|
||||
mode={mode}
|
||||
storyLabel="2 envs"
|
||||
/>
|
||||
|
||||
<Variant
|
||||
@@ -74,8 +74,8 @@ function Story() {
|
||||
"react-native",
|
||||
"unity",
|
||||
]}
|
||||
storyLabel="5 envs"
|
||||
mode={mode}
|
||||
storyLabel="5 envs"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -92,23 +92,23 @@ function Variant(props: {
|
||||
return (
|
||||
<BadgeContainer label={props.storyLabel}>
|
||||
<CodeSegment
|
||||
environment={env}
|
||||
hideTabs={props.mode === "no-tabs"}
|
||||
onlyTabs={props.mode === "only-tabs"}
|
||||
setEnvironment={setEnv}
|
||||
snippet={{
|
||||
javascript: props.envsToShow.includes("javascript")
|
||||
? jsCode
|
||||
: undefined,
|
||||
react: props.envsToShow.includes("react") ? jsxCode : undefined,
|
||||
unity: props.envsToShow.includes("unity") ? unityCode : undefined,
|
||||
"react-native": props.envsToShow.includes("react-native")
|
||||
? jsxCode
|
||||
: undefined,
|
||||
typescript: props.envsToShow.includes("typescript")
|
||||
? tsCode
|
||||
: undefined,
|
||||
unity: props.envsToShow.includes("unity") ? unityCode : undefined,
|
||||
}}
|
||||
environment={env}
|
||||
setEnvironment={setEnv}
|
||||
hideTabs={props.mode === "no-tabs"}
|
||||
onlyTabs={props.mode === "only-tabs"}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
|
||||
@@ -20,9 +20,9 @@ export function DismissibleAlert(props: {
|
||||
return (
|
||||
<div className="relative rounded-lg border border-border bg-card p-4">
|
||||
<Button
|
||||
onClick={() => setIsVisible(false)}
|
||||
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
|
||||
aria-label="Close alert"
|
||||
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
|
||||
onClick={() => setIsVisible(false)}
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
|
||||
@@ -38,11 +38,11 @@ export function DistributionBarChart(props: DistributionBarChartProps) {
|
||||
{props.segments.map((segment) => {
|
||||
return (
|
||||
<div
|
||||
key={segment.label}
|
||||
className="flex h-full items-center justify-center transition-all duration-200"
|
||||
key={segment.label}
|
||||
style={{
|
||||
width: `${segment.percent}%`,
|
||||
backgroundColor: segment.color,
|
||||
width: `${segment.percent}%`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -53,7 +53,7 @@ export function DistributionBarChart(props: DistributionBarChartProps) {
|
||||
<div className="mt-3 flex flex-col gap-1 lg:flex-row lg:gap-6">
|
||||
{props.segments.map((segment) => {
|
||||
return (
|
||||
<div key={segment.label} className="flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5" key={segment.label}>
|
||||
<div
|
||||
className="size-3 rounded-full"
|
||||
style={{
|
||||
|
||||
@@ -2,7 +2,6 @@ import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { DropZone } from "./drop-zone";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/DropZone",
|
||||
component: DropZone,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
@@ -11,6 +10,7 @@ const meta = {
|
||||
</div>
|
||||
),
|
||||
],
|
||||
title: "blocks/DropZone",
|
||||
} satisfies Meta<typeof DropZone>;
|
||||
|
||||
export default meta;
|
||||
@@ -18,36 +18,36 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
accept: undefined,
|
||||
description: "This is a description for drop zone",
|
||||
isError: false,
|
||||
onDrop: () => {},
|
||||
title: "This is a title",
|
||||
description: "This is a description for drop zone",
|
||||
accept: undefined,
|
||||
resetButton: undefined,
|
||||
title: "This is a title",
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorState: Story = {
|
||||
args: {
|
||||
accept: undefined,
|
||||
description: "This is a description",
|
||||
isError: true,
|
||||
onDrop: () => {},
|
||||
title: "this is title",
|
||||
description: "This is a description",
|
||||
accept: undefined,
|
||||
resetButton: undefined,
|
||||
title: "this is title",
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorStateWithResetButton: Story = {
|
||||
args: {
|
||||
accept: undefined,
|
||||
description: "This is a description",
|
||||
isError: true,
|
||||
onDrop: () => {},
|
||||
title: "this is title",
|
||||
description: "This is a description",
|
||||
accept: undefined,
|
||||
resetButton: {
|
||||
label: "Remove Files",
|
||||
onClick: () => {},
|
||||
},
|
||||
title: "this is title",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { UploadIcon, XIcon } from "lucide-react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function DropZone(props: {
|
||||
isError: boolean;
|
||||
@@ -64,11 +64,11 @@ export function DropZone(props: {
|
||||
{resetButton && (
|
||||
<Button
|
||||
className="relative z-50 mt-4"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
resetButton.onClick();
|
||||
}}
|
||||
size="sm"
|
||||
>
|
||||
{resetButton.label}
|
||||
</Button>
|
||||
|
||||
+5
-5
@@ -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: {
|
||||
</ScrollShadow>
|
||||
|
||||
<CopyTextButton
|
||||
copyIconPosition="left"
|
||||
textToShow="Copy value"
|
||||
textToCopy={stringifiedValue}
|
||||
tooltip="Copy value"
|
||||
className="-translate-x-2 mt-1 text-muted-foreground"
|
||||
copyIconPosition="left"
|
||||
textToCopy={stringifiedValue}
|
||||
textToShow="Copy value"
|
||||
tooltip="Copy value"
|
||||
variant="ghost"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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<typeof Story>;
|
||||
|
||||
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 (
|
||||
<div className="container flex max-w-6xl flex-col gap-6 py-10">
|
||||
<VariantTest defaultValues={[]} storyLabel="5 items" listLen={5} />
|
||||
<VariantTest defaultValues={[]} storyLabel="5000 items" listLen={5000} />
|
||||
<VariantTest defaultValues={[]} listLen={5} storyLabel="5 items" />
|
||||
<VariantTest defaultValues={[]} listLen={5000} storyLabel="5000 items" />
|
||||
<VariantTest
|
||||
defaultValues={["1", "3", "5"]}
|
||||
storyLabel="20 items, 3 selected by default"
|
||||
listLen={20}
|
||||
storyLabel="20 items, 3 selected by default"
|
||||
/>
|
||||
<VariantTest
|
||||
defaultValues={["1", "2", "3", "4", "5", "6", "7"]}
|
||||
storyLabel="20 items, 7 selected, show max 3"
|
||||
listLen={20}
|
||||
maxCount={3}
|
||||
storyLabel="20 items, 7 selected, show max 3"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -59,13 +59,13 @@ function VariantTest(props: {
|
||||
return (
|
||||
<BadgeContainer label={props.storyLabel}>
|
||||
<MultiSelect
|
||||
selectedValues={values}
|
||||
options={list}
|
||||
maxCount={props.maxCount}
|
||||
onSelectedValuesChange={(values) => {
|
||||
setValues(values);
|
||||
}}
|
||||
options={list}
|
||||
placeholder="Select items"
|
||||
maxCount={props.maxCount}
|
||||
selectedValues={values}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
|
||||
@@ -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<HTMLButtonElement> {
|
||||
@@ -150,58 +143,48 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
|
||||
// scroll to top when options change
|
||||
const popoverElRef = useRef<HTMLDivElement>(null);
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
useEffect(() => {
|
||||
const scrollContainer =
|
||||
popoverElRef.current?.querySelector("[data-scrollable]");
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
|
||||
<Popover modal onOpenChange={setIsPopoverOpen} open={isPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
{props.customTrigger || (
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
|
||||
className,
|
||||
)}
|
||||
onClick={handleTogglePopover}
|
||||
>
|
||||
{selectedValues.length > 0 ? (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<SelectedChainsBadges
|
||||
selectedValues={selectedValues}
|
||||
options={options}
|
||||
clearExtraOptions={clearExtraOptions}
|
||||
maxCount={maxCount}
|
||||
onClose={handleClear}
|
||||
options={options}
|
||||
selectedValues={selectedValues}
|
||||
toggleOption={toggleOption}
|
||||
clearExtraOptions={clearExtraOptions}
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Clear All */}
|
||||
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
|
||||
{/* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */}
|
||||
{/* biome-ignore lint/a11y/useKeyWithClickEvents: FIXME */}
|
||||
{/* biome-ignore lint/a11y/useFocusableInteractive: FIXME */}
|
||||
<div
|
||||
role="button"
|
||||
className="rounded p-1 hover:bg-accent"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleClear();
|
||||
}}
|
||||
className="rounded p-1 hover:bg-accent"
|
||||
role="button"
|
||||
>
|
||||
<XIcon className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="flex h-full min-h-6"
|
||||
orientation="vertical"
|
||||
/>
|
||||
<ChevronDownIcon className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
@@ -218,20 +201,20 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align={props.align}
|
||||
className={cn(
|
||||
"flex max-h-[500px] flex-col p-0",
|
||||
popoverContentClassName,
|
||||
)}
|
||||
align={props.align}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
ref={popoverElRef}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
style={{
|
||||
width: "var(--radix-popover-trigger-width)",
|
||||
height:
|
||||
"calc(var(--radix-popover-content-available-height) - 40px)",
|
||||
width: "var(--radix-popover-trigger-width)",
|
||||
}}
|
||||
ref={popoverElRef}
|
||||
>
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
@@ -239,13 +222,22 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
customSearchInput
|
||||
) : (
|
||||
<Input
|
||||
placeholder={searchPlaceholder || "Search"}
|
||||
value={searchValue}
|
||||
// do not focus on the input when the popover opens to avoid opening the keyboard
|
||||
tabIndex={-1}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
className="!h-auto rounded-b-none border-0 border-border border-b py-4 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
onChange={(e) => {
|
||||
setSearchValue(e.target.value);
|
||||
const scrollContainer =
|
||||
popoverElRef.current?.querySelector("[data-scrollable]");
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
}}
|
||||
// do not focus on the input when the popover opens to avoid opening the keyboard
|
||||
onKeyDown={handleInputKeyDown}
|
||||
placeholder={searchPlaceholder || "Search"}
|
||||
tabIndex={-1}
|
||||
value={searchValue}
|
||||
/>
|
||||
)}
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
|
||||
@@ -259,8 +251,8 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
|
||||
{optionsToShow.length > 0 && (
|
||||
<ScrollShadow
|
||||
scrollableClassName="p-1 h-full"
|
||||
className="flex-1 rounded"
|
||||
scrollableClassName="p-1 h-full"
|
||||
>
|
||||
{/* List */}
|
||||
<div>
|
||||
@@ -268,15 +260,15 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
const isSelected = selectedValues.includes(option.value);
|
||||
return (
|
||||
<Button
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => toggleOption(option.value)}
|
||||
variant="ghost"
|
||||
className="flex w-full cursor-pointer justify-start gap-3 rounded-sm px-3 py-2 text-left"
|
||||
key={option.value}
|
||||
onClick={() => toggleOption(option.value)}
|
||||
ref={
|
||||
i === optionsToShow.length - 1 ? lastItemRef : undefined
|
||||
}
|
||||
role="option"
|
||||
variant="ghost"
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
@@ -302,12 +294,12 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
{showSelectedValuesInModal && selectedValues.length > 0 && (
|
||||
<div className="border-t px-3 py-3">
|
||||
<SelectedChainsBadges
|
||||
selectedValues={selectedValues}
|
||||
options={options}
|
||||
clearExtraOptions={clearExtraOptions}
|
||||
maxCount={maxCount}
|
||||
onClose={handleClear}
|
||||
options={options}
|
||||
selectedValues={selectedValues}
|
||||
toggleOption={toggleOption}
|
||||
clearExtraOptions={clearExtraOptions}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
@@ -317,10 +309,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
},
|
||||
);
|
||||
|
||||
function ClosableBadge(props: {
|
||||
label: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
function ClosableBadge(props: { label: string; onClose: () => void }) {
|
||||
return (
|
||||
<span className="flex items-center gap-3 rounded-xl border border-border bg-muted px-2 py-1 text-foreground text-xs">
|
||||
{props.label}
|
||||
|
||||
+11
-11
@@ -2,7 +2,6 @@ import type { Meta, StoryObj } from "@storybook/nextjs";
|
||||
import { MultiStepStatus } from "./multi-step-status";
|
||||
|
||||
const meta = {
|
||||
title: "Blocks/MultiStepStatus",
|
||||
component: MultiStepStatus,
|
||||
decorators: [
|
||||
(Story) => (
|
||||
@@ -11,6 +10,7 @@ const meta = {
|
||||
</div>
|
||||
),
|
||||
],
|
||||
title: "Blocks/MultiStepStatus",
|
||||
} satisfies Meta<typeof MultiStepStatus>;
|
||||
|
||||
export default meta;
|
||||
@@ -21,29 +21,29 @@ export const AllStates: Story = {
|
||||
onRetry: () => {},
|
||||
steps: [
|
||||
{
|
||||
status: { type: "completed" },
|
||||
label: "Connect Wallet",
|
||||
id: "connect-wallet",
|
||||
label: "Connect Wallet",
|
||||
status: { type: "completed" },
|
||||
},
|
||||
{
|
||||
status: { type: "pending" },
|
||||
label: "Sign Message",
|
||||
id: "sign-message",
|
||||
label: "Sign Message",
|
||||
status: { type: "pending" },
|
||||
},
|
||||
{
|
||||
status: { type: "error", message: "This is an error message" },
|
||||
label: "Approve Transaction",
|
||||
id: "approve-transaction",
|
||||
label: "Approve Transaction",
|
||||
status: { message: "This is an error message", type: "error" },
|
||||
},
|
||||
{
|
||||
status: { type: "idle" },
|
||||
label: "Confirm Transaction",
|
||||
id: "confirm-transaction",
|
||||
label: "Confirm Transaction",
|
||||
status: { type: "idle" },
|
||||
},
|
||||
{
|
||||
status: { type: "idle" },
|
||||
label: "Finalize",
|
||||
id: "finalize",
|
||||
label: "Finalize",
|
||||
status: { type: "idle" },
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
AlertCircleIcon,
|
||||
CircleCheckIcon,
|
||||
CircleIcon,
|
||||
RefreshCwIcon,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "../../ui/DynamicHeight";
|
||||
import { Spinner } from "../../ui/Spinner/Spinner";
|
||||
|
||||
@@ -32,7 +32,7 @@ export function MultiStepStatus<T extends string>(props: {
|
||||
<DynamicHeight>
|
||||
<div className="space-y-4">
|
||||
{props.steps.map((step) => (
|
||||
<div key={step.label} className="flex items-start space-x-3 ">
|
||||
<div className="flex items-start space-x-3 " key={step.label}>
|
||||
{step.status.type === "completed" ? (
|
||||
<CircleCheckIcon className="mt-0.5 size-5 flex-shrink-0 text-green-500" />
|
||||
) : step.status.type === "pending" ? (
|
||||
@@ -72,10 +72,10 @@ export function MultiStepStatus<T extends string>(props: {
|
||||
{step.status.message}
|
||||
</p>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
onClick={() => props.onRetry(step)}
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
>
|
||||
<RefreshCwIcon className="size-4" />
|
||||
Retry
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { BellIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Drawer,
|
||||
@@ -13,8 +15,6 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { BellIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { NotificationList } from "./notification-list";
|
||||
import { useNotifications } from "./state/manager";
|
||||
|
||||
@@ -26,7 +26,7 @@ export function NotificationsButton(props: { accountId: string }) {
|
||||
|
||||
const trigger = useMemo(
|
||||
() => (
|
||||
<Button variant="outline" size="icon" className="relative rounded-full">
|
||||
<Button className="relative rounded-full" size="icon" variant="outline">
|
||||
<BellIcon className="h-4 w-4" />
|
||||
{(manager.unreadNotificationsCount || 0) > 0 && (
|
||||
<span className="absolute top-0 right-0 flex h-2 w-2">
|
||||
@@ -41,7 +41,7 @@ export function NotificationsButton(props: { accountId: string }) {
|
||||
|
||||
if (isMobile) {
|
||||
return (
|
||||
<Drawer open={open} onOpenChange={setOpen}>
|
||||
<Drawer onOpenChange={setOpen} open={open}>
|
||||
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[90vh] min-h-[66vh]">
|
||||
<DrawerTitle className="sr-only">Notifications</DrawerTitle>
|
||||
@@ -52,11 +52,11 @@ export function NotificationsButton(props: { accountId: string }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<Popover onOpenChange={setOpen} open={open}>
|
||||
<PopoverTrigger asChild>{trigger}</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="max-h-[90vh] min-h-[500px] w-[400px] max-w-md p-0"
|
||||
align="end"
|
||||
className="max-h-[90vh] min-h-[500px] w-[400px] max-w-md p-0"
|
||||
>
|
||||
<NotificationList {...manager} />
|
||||
</PopoverContent>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { Notification } from "@/api/notifications";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
format,
|
||||
formatDistanceToNow,
|
||||
@@ -11,6 +9,8 @@ import {
|
||||
} from "date-fns";
|
||||
import { ArchiveIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import type { Notification } from "@/api/notifications";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
interface NotificationEntryProps {
|
||||
notification: Notification;
|
||||
@@ -54,11 +54,11 @@ export function NotificationEntry({
|
||||
<p className="text-muted-foreground text-xs">{timeAgo}</p>
|
||||
)}
|
||||
<div className="flex flex-row justify-between gap-2 pt-1">
|
||||
<Button asChild variant="link" size="sm" className="px-0">
|
||||
<Button asChild className="px-0" size="sm" variant="link">
|
||||
<a
|
||||
href={notification.ctaUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{notification.ctaText}
|
||||
</a>
|
||||
@@ -68,10 +68,10 @@ export function NotificationEntry({
|
||||
</div>
|
||||
{onMarkAsRead && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => onMarkAsRead(notification.id)}
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
onClick={() => onMarkAsRead(notification.id)}
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
>
|
||||
<ArchiveIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
@@ -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<typeof useNotifications>) {
|
||||
return (
|
||||
<div className="flex w-full flex-1 flex-col">
|
||||
<TabButtons
|
||||
tabContainerClassName="space-x-6 px-4 pt-2"
|
||||
tabClassName="!text-sm hover:!bg-transparent !px-0 !py-1"
|
||||
tabContainerClassName="space-x-6 px-4 pt-2"
|
||||
tabs={[
|
||||
{
|
||||
isActive: activeTab === "inbox",
|
||||
name: (
|
||||
<div className="flex items-center gap-1">
|
||||
Inbox
|
||||
@@ -38,12 +39,11 @@ export function NotificationList(props: ReturnType<typeof useNotifications>) {
|
||||
</div>
|
||||
),
|
||||
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<typeof useNotifications>) {
|
||||
>
|
||||
{activeTab === "inbox" ? (
|
||||
<InboxTab
|
||||
unreadNotifications={props.unreadNotifications}
|
||||
isLoadingUnread={props.isLoadingUnread}
|
||||
hasMoreUnread={props.hasMoreUnread}
|
||||
isFetchingMoreUnread={props.isFetchingMoreUnread}
|
||||
isLoadingUnread={props.isLoadingUnread}
|
||||
loadMoreUnread={props.loadMoreUnread}
|
||||
markAsRead={props.markAsRead}
|
||||
markAllAsRead={props.markAllAsRead}
|
||||
markAsRead={props.markAsRead}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
unreadNotifications={props.unreadNotifications}
|
||||
/>
|
||||
) : (
|
||||
<ArchiveTab
|
||||
archivedNotifications={props.archivedNotifications}
|
||||
isLoadingArchived={props.isLoadingArchived}
|
||||
hasMoreArchived={props.hasMoreArchived}
|
||||
isFetchingMoreArchived={props.isFetchingMoreArchived}
|
||||
isLoadingArchived={props.isLoadingArchived}
|
||||
loadMoreArchived={props.loadMoreArchived}
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
/>
|
||||
@@ -110,8 +110,8 @@ function InboxTab(
|
||||
))}
|
||||
<AutoLoadMore
|
||||
hasMore={props.hasMoreUnread}
|
||||
loadMore={props.loadMoreUnread}
|
||||
isLoading={props.isFetchingMoreUnread || props.isLoadingUnread}
|
||||
loadMore={props.loadMoreUnread}
|
||||
scrollContainerRef={props.scrollContainerRef}
|
||||
/>
|
||||
</>
|
||||
@@ -144,8 +144,8 @@ function ArchiveTab(
|
||||
))}
|
||||
<AutoLoadMore
|
||||
hasMore={props.hasMoreArchived}
|
||||
loadMore={props.loadMoreArchived}
|
||||
isLoading={props.isFetchingMoreArchived || props.isLoadingArchived}
|
||||
loadMore={props.loadMoreArchived}
|
||||
scrollContainerRef={props.scrollContainerRef}
|
||||
/>
|
||||
</>
|
||||
@@ -198,5 +198,5 @@ function AutoLoadMore(props: {
|
||||
);
|
||||
}
|
||||
|
||||
return <div ref={ref} className="h-[1px] w-full opacity-0" aria-hidden />;
|
||||
return <div aria-hidden className="h-[1px] w-full opacity-0" ref={ref} />;
|
||||
}
|
||||
|
||||
@@ -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<NotificationsApiResponse>({
|
||||
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<NotificationsApiResponse>({
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<PricingCardProps> = ({
|
||||
{cta && (
|
||||
<div className="flex flex-col gap-3">
|
||||
{cta.type === "renew" && (
|
||||
<RenewSubscriptionButton teamId={teamId} getTeam={getTeam} />
|
||||
<RenewSubscriptionButton getTeam={getTeam} teamId={teamId} />
|
||||
)}
|
||||
{billingPlanToSkuMap[billingPlan] && cta.type === "checkout" && (
|
||||
<CheckoutButton
|
||||
billingStatus={billingStatus}
|
||||
buttonProps={{
|
||||
variant: highlighted ? "default" : "outline",
|
||||
className: highlighted ? undefined : "bg-background",
|
||||
onClick: cta.onClick,
|
||||
variant: highlighted ? "default" : "outline",
|
||||
}}
|
||||
teamSlug={teamSlug}
|
||||
sku={billingPlanToSkuMap[billingPlan]}
|
||||
teamSlug={teamSlug}
|
||||
>
|
||||
{has7DayTrial ? "Start 7 Day Free Trial" : cta.label}
|
||||
</CheckoutButton>
|
||||
@@ -160,15 +160,15 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
|
||||
{cta.type === "link" && (
|
||||
<Button
|
||||
variant={highlighted ? "default" : "outline"}
|
||||
className={highlighted ? undefined : "bg-background"}
|
||||
asChild
|
||||
className={highlighted ? undefined : "bg-background"}
|
||||
variant={highlighted ? "default" : "outline"}
|
||||
>
|
||||
<Link
|
||||
href={cta.href}
|
||||
target="_blank"
|
||||
onClick={cta.onClick}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
{has7DayTrial ? "Start 7 Day Free Trial" : cta.label}
|
||||
</Link>
|
||||
@@ -188,14 +188,14 @@ export const PricingCard: React.FC<PricingCardProps> = ({
|
||||
|
||||
const billingPlanToSkuMap: Record<Team["billingPlan"], ProductSKU | undefined> =
|
||||
{
|
||||
starter: "plan:starter",
|
||||
growth: "plan:growth",
|
||||
scale: "plan:scale",
|
||||
pro: "plan:pro",
|
||||
// we can't render checkout buttons for these plans:
|
||||
accelerate: undefined,
|
||||
free: undefined,
|
||||
growth: "plan:growth",
|
||||
growth_legacy: undefined,
|
||||
pro: "plan:pro",
|
||||
scale: "plan:scale",
|
||||
starter: "plan:starter",
|
||||
};
|
||||
|
||||
type FeatureItemProps = {
|
||||
|
||||
@@ -4,13 +4,13 @@ import { BadgeContainer } from "../../../stories/utils";
|
||||
import { SelectWithSearch } from "./select-with-search";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/SelectWithSearch",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/SelectWithSearch",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -22,20 +22,20 @@ export const Variants: Story = {
|
||||
|
||||
function createList(len: number) {
|
||||
return Array.from({ length: len }, (_, i) => ({
|
||||
value: `${i}`,
|
||||
label: `Item ${i}`,
|
||||
value: `${i}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="container flex max-w-6xl flex-col gap-6 py-10">
|
||||
<VariantTest storyLabel="5 items" listLen={5} />
|
||||
<VariantTest storyLabel="5000 items" listLen={5000} />
|
||||
<VariantTest listLen={5} storyLabel="5 items" />
|
||||
<VariantTest listLen={5000} storyLabel="5000 items" />
|
||||
<VariantTest
|
||||
defaultValue={"3"}
|
||||
storyLabel="20 items, 3 selected by default"
|
||||
listLen={20}
|
||||
storyLabel="20 items, 3 selected by default"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -52,10 +52,10 @@ function VariantTest(props: {
|
||||
return (
|
||||
<BadgeContainer label={props.storyLabel}>
|
||||
<SelectWithSearch
|
||||
value={value}
|
||||
options={list}
|
||||
onValueChange={setValue}
|
||||
options={list}
|
||||
placeholder="Select items"
|
||||
value={value}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
"use client";
|
||||
|
||||
import { CheckIcon, ChevronDownIcon, SearchIcon } from "lucide-react";
|
||||
import React, { useMemo, useRef } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
@@ -8,11 +9,9 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, ChevronDownIcon, SearchIcon } from "lucide-react";
|
||||
import React, { useRef, useMemo, useEffect } 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 SelectWithSearchProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@@ -101,28 +100,18 @@ export const SelectWithSearch = React.forwardRef<
|
||||
|
||||
// scroll to top when options change
|
||||
const popoverElRef = useRef<HTMLDivElement>(null);
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
|
||||
useEffect(() => {
|
||||
const scrollContainer =
|
||||
popoverElRef.current?.querySelector("[data-scrollable]");
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
}, [searchValue]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen} modal>
|
||||
<Popover modal onOpenChange={setIsPopoverOpen} open={isPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={() => setIsPopoverOpen(true)}
|
||||
className={cn(
|
||||
"flex w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
|
||||
className,
|
||||
)}
|
||||
onClick={() => setIsPopoverOpen(true)}
|
||||
>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<span
|
||||
@@ -141,32 +130,41 @@ export const SelectWithSearch = React.forwardRef<
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className={cn("p-0", popoverContentClassName)}
|
||||
align={props.align || "center"}
|
||||
className={cn("p-0", popoverContentClassName)}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
ref={popoverElRef}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
style={{
|
||||
width: "var(--radix-popover-trigger-width)",
|
||||
maxHeight: "var(--radix-popover-content-available-height)",
|
||||
width: "var(--radix-popover-trigger-width)",
|
||||
}}
|
||||
ref={popoverElRef}
|
||||
>
|
||||
<div>
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="!h-auto rounded-b-none border-0 border-border border-b py-3 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
onChange={(e) => {
|
||||
setSearchValue(e.target.value);
|
||||
const scrollContainer =
|
||||
popoverElRef.current?.querySelector("[data-scrollable]");
|
||||
if (scrollContainer) {
|
||||
scrollContainer.scrollTo({
|
||||
top: 0,
|
||||
});
|
||||
}
|
||||
}}
|
||||
placeholder={searchPlaceholder || "Search"}
|
||||
value={searchValue}
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
className="!h-auto rounded-b-none border-0 border-border border-b py-3 pl-10 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
/>
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<ScrollShadow
|
||||
scrollableClassName="max-h-[min(calc(var(--radix-popover-content-available-height)-60px),350px)] p-1"
|
||||
className="rounded"
|
||||
scrollableClassName="max-h-[min(calc(var(--radix-popover-content-available-height)-60px),350px)] p-1"
|
||||
>
|
||||
{/* List */}
|
||||
<div>
|
||||
@@ -180,20 +178,21 @@ export const SelectWithSearch = React.forwardRef<
|
||||
const isSelected = value === option.value;
|
||||
return (
|
||||
<Button
|
||||
key={option.value}
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
className="flex w-full cursor-pointer justify-start gap-3 rounded-sm px-3 py-2 text-left"
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
onValueChange(option.value);
|
||||
if (closeOnSelect) {
|
||||
setIsPopoverOpen(false);
|
||||
}
|
||||
}}
|
||||
variant="ghost"
|
||||
className="flex w-full cursor-pointer justify-start gap-3 rounded-sm px-3 py-2 text-left"
|
||||
ref={
|
||||
i === optionsToShow.length - 1 ? lastItemRef : undefined
|
||||
}
|
||||
// biome-ignore lint/a11y/useSemanticElements: TDOO
|
||||
role="option"
|
||||
variant="ghost"
|
||||
>
|
||||
{showCheck && (
|
||||
<div className="flex size-4 items-center justify-center">
|
||||
|
||||
@@ -3,11 +3,7 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
export function GenericLoadingPage({
|
||||
className,
|
||||
}: {
|
||||
className?: string;
|
||||
}) {
|
||||
export function GenericLoadingPage({ className }: { className?: string }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { CrownIcon, LockIcon, SparklesIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
@@ -9,9 +12,6 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CrownIcon, LockIcon, SparklesIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type React from "react";
|
||||
import { TeamPlanBadge } from "../../../app/(app)/components/TeamPlanBadge";
|
||||
import type { Team } from "../../api/team";
|
||||
import { Badge } from "../ui/badge";
|
||||
@@ -69,8 +69,8 @@ export function UpsellWrapper({
|
||||
<div className="space-y-2">
|
||||
<TeamPlanBadge
|
||||
plan="scale"
|
||||
teamSlug={teamSlug}
|
||||
postfix=" Feature"
|
||||
teamSlug={teamSlug}
|
||||
/>
|
||||
<CardTitle className="font-bold text-2xl text-foreground md:text-3xl">
|
||||
Unlock {featureName}
|
||||
@@ -90,15 +90,15 @@ export function UpsellWrapper({
|
||||
<div className="grid gap-2">
|
||||
{benefits.map((benefit) => (
|
||||
<div
|
||||
key={benefit.description}
|
||||
className="flex items-center gap-3"
|
||||
key={benefit.description}
|
||||
>
|
||||
<div className="flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-accent">
|
||||
<SparklesIcon className="h-3 w-3 text-success-text" />
|
||||
</div>
|
||||
<span className="text-sm">{benefit.description}</span>
|
||||
{benefit.status === "soon" && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
<Badge className="text-xs" variant="secondary">
|
||||
Coming Soon
|
||||
</Badge>
|
||||
)}
|
||||
@@ -109,7 +109,7 @@ export function UpsellWrapper({
|
||||
)}
|
||||
|
||||
<div className="flex flex-col gap-3 pt-4 sm:flex-row">
|
||||
<Button className="flex-1 py-3 font-semibold" size="lg" asChild>
|
||||
<Button asChild className="flex-1 py-3 font-semibold" size="lg">
|
||||
<Link
|
||||
href={`/team/${teamSlug}/~/settings/billing?showPlans=true&highlight=${requiredPlan}`}
|
||||
>
|
||||
@@ -118,7 +118,7 @@ export function UpsellWrapper({
|
||||
<span className="ml-1 capitalize">{requiredPlan}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="outline" size="lg" className="md:flex-1" asChild>
|
||||
<Button asChild className="md:flex-1" size="lg" variant="outline">
|
||||
<Link
|
||||
href={`/team/${teamSlug}/~/settings/billing?showPlans=true`}
|
||||
>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
"use client";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
||||
import {
|
||||
HoverCard,
|
||||
@@ -6,12 +11,6 @@ import {
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { resolveSchemeWithErrorHandler } from "@/lib/resolveSchemeWithErrorHandler";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { type ThirdwebClient, isAddress } from "thirdweb";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { Badge } from "../ui/badge";
|
||||
import { Button } from "../ui/button";
|
||||
@@ -47,7 +46,7 @@ export function WalletAddress(props: {
|
||||
|
||||
if (!isAddress(address)) {
|
||||
return (
|
||||
<ToolTipLabel label={address} hoverable>
|
||||
<ToolTipLabel hoverable label={address}>
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
|
||||
<div className="flex size-6 items-center justify-center rounded-full border bg-background">
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
@@ -70,19 +69,19 @@ export function WalletAddress(props: {
|
||||
tabIndex={props.preventOpenOnFocus ? -1 : undefined}
|
||||
>
|
||||
<Button
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
variant="link"
|
||||
className={cn(
|
||||
"flex flex-row items-center gap-2 px-0",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
variant="link"
|
||||
>
|
||||
{address && (
|
||||
<WalletAvatar
|
||||
address={address}
|
||||
iconClassName={props.iconClassName}
|
||||
profiles={profiles.data || []}
|
||||
thirdwebClient={props.client}
|
||||
iconClassName={props.iconClassName}
|
||||
/>
|
||||
)}
|
||||
<span className="cursor-pointer font-mono">
|
||||
@@ -101,10 +100,10 @@ export function WalletAddress(props: {
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="font-semibold text-lg">Wallet Address</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onCopy}
|
||||
className="flex items-center gap-2"
|
||||
onClick={onCopy}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon className="h-4 w-4" />
|
||||
@@ -137,9 +136,9 @@ export function WalletAddress(props: {
|
||||
{walletAvatarLink && (
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src={walletAvatarLink}
|
||||
alt={profile.name}
|
||||
className="object-cover"
|
||||
src={walletAvatarLink}
|
||||
/>
|
||||
{profile.name && (
|
||||
<AvatarFallback>
|
||||
@@ -197,14 +196,14 @@ function WalletAvatar(props: {
|
||||
>
|
||||
{resolvedAvatarSrc ? (
|
||||
<Img
|
||||
src={resolvedAvatarSrc}
|
||||
className={cn("size-6 object-cover", props.iconClassName)}
|
||||
src={resolvedAvatarSrc}
|
||||
/>
|
||||
) : (
|
||||
<Blobbie
|
||||
address={props.address}
|
||||
size={24}
|
||||
className={props.iconClassName}
|
||||
size={24}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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={<Skeleton className="size-[34px] rounded-full border bg-accent" />}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
aria-label="Toggle theme"
|
||||
className="h-auto w-auto rounded-full border border-border bg-background p-2"
|
||||
onClick={() => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
}}
|
||||
aria-label="Toggle theme"
|
||||
className="h-auto w-auto rounded-full border border-border bg-background p-2"
|
||||
variant="outline"
|
||||
>
|
||||
{theme === "light" ? (
|
||||
<SunIcon className="size-4" />
|
||||
|
||||
@@ -4,13 +4,13 @@ import { BadgeContainer } from "stories/utils";
|
||||
import { PaginationButtons } from "./pagination-buttons";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/PaginationButtons",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
title: "blocks/PaginationButtons",
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
@@ -32,10 +32,7 @@ function Story() {
|
||||
);
|
||||
}
|
||||
|
||||
function Variant(props: {
|
||||
label: string;
|
||||
totalPages: number;
|
||||
}) {
|
||||
function Variant(props: { label: string; totalPages: number }) {
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
return (
|
||||
<BadgeContainer label={props.label}>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
@@ -9,8 +11,6 @@ import {
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from "@/components/ui/pagination";
|
||||
import { ArrowUpRightIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
@@ -179,27 +179,27 @@ export const PaginationButtons = (props: {
|
||||
|
||||
<div className="relative flex items-center">
|
||||
<Input
|
||||
value={pageNumberInput}
|
||||
onChange={(e) => {
|
||||
setInputHasError(false);
|
||||
setPageNumberInput(e.target.value);
|
||||
}}
|
||||
type="number"
|
||||
placeholder="Page"
|
||||
className={cn(
|
||||
"w-[60px] bg-transparent [appearance:textfield] max-sm:placeholder:text-sm lg:w-[100px] lg:pr-8 [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none",
|
||||
inputHasError && "text-red-500",
|
||||
)}
|
||||
onChange={(e) => {
|
||||
setInputHasError(false);
|
||||
setPageNumberInput(e.target.value);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handlePageInputSubmit();
|
||||
}
|
||||
}}
|
||||
placeholder="Page"
|
||||
type="number"
|
||||
value={pageNumberInput}
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="absolute right-1 h-auto w-auto p-2 max-sm:hidden"
|
||||
onClick={handlePageInputSubmit}
|
||||
variant="ghost"
|
||||
>
|
||||
<ArrowUpRightIcon className="size-4" />
|
||||
</Button>
|
||||
|
||||
@@ -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<typeof buttonVariants> {
|
||||
@@ -36,7 +36,7 @@ export function ConfirmationDialog({
|
||||
className,
|
||||
}: ConfirmationDialogProps) {
|
||||
return (
|
||||
<AlertDialog open={open} onOpenChange={onOpenChange}>
|
||||
<AlertDialog onOpenChange={onOpenChange} open={open}>
|
||||
<AlertDialogTrigger asChild>{children}</AlertDialogTrigger>
|
||||
|
||||
<AlertDialogContent className={cn("w-[90vw] sm:w-auto", className)}>
|
||||
@@ -48,12 +48,12 @@ export function ConfirmationDialog({
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
|
||||
<AlertDialogAction
|
||||
variant={variant}
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
onSubmit?.();
|
||||
onOpenChange?.(false);
|
||||
}}
|
||||
type="submit"
|
||||
variant={variant}
|
||||
>
|
||||
Continue
|
||||
</AlertDialogAction>
|
||||
|
||||
@@ -20,12 +20,12 @@ export function CopyAddressButton(props: {
|
||||
|
||||
return (
|
||||
<CopyTextButton
|
||||
className={cn("font-mono text-sm", props.className)}
|
||||
copyIconPosition={props.copyIconPosition}
|
||||
textToCopy={props.address}
|
||||
textToShow={shortenedAddress}
|
||||
tooltip="Copy Address"
|
||||
className={cn("font-mono text-sm", props.className)}
|
||||
variant={props.variant}
|
||||
copyIconPosition={props.copyIconPosition}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<ToolTipLabel label="Copy">
|
||||
<Button
|
||||
variant={props.variant || "ghost"}
|
||||
aria-label="Copy"
|
||||
className={cn("h-auto w-auto p-1", props.className)}
|
||||
onClick={onCopy}
|
||||
variant={props.variant || "ghost"}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<CheckIcon
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -39,7 +39,6 @@ export function CopyTextButton(props: {
|
||||
return (
|
||||
<ToolTipLabel label={props.tooltip}>
|
||||
<Button
|
||||
variant={props.variant || "outline"}
|
||||
aria-label={props.tooltip}
|
||||
className={cn(
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-1.5 py-0.5 font-normal text-foreground",
|
||||
@@ -50,6 +49,7 @@ export function CopyTextButton(props: {
|
||||
e.stopPropagation();
|
||||
props.onClick?.();
|
||||
}}
|
||||
variant={props.variant || "outline"}
|
||||
>
|
||||
{props.copyIconPosition === "right" ? (
|
||||
<>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { format, isBefore } from "date-fns";
|
||||
import { CalendarIcon, CalendarX2Icon, ChevronDownIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
@@ -8,9 +11,6 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { format, isBefore } from "date-fns";
|
||||
import { CalendarIcon, CalendarX2Icon, ChevronDownIcon } from "lucide-react";
|
||||
import React from "react";
|
||||
import { DynamicHeight } from "./DynamicHeight";
|
||||
import { TabButtons } from "./tabs";
|
||||
|
||||
@@ -33,23 +33,22 @@ export function DatePickerWithRange(props: {
|
||||
|
||||
return (
|
||||
<Popover
|
||||
open={isOpen}
|
||||
onOpenChange={(v) => {
|
||||
if (!v) {
|
||||
setScreen("from");
|
||||
}
|
||||
setIsOpen(v);
|
||||
}}
|
||||
open={isOpen}
|
||||
>
|
||||
{/* Button */}
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date"
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"justify-start gap-2 text-left font-normal",
|
||||
props.className,
|
||||
)}
|
||||
variant="outline"
|
||||
>
|
||||
<CalendarIcon className="h-4 w-4" />
|
||||
{props.labelOverride || (
|
||||
@@ -63,8 +62,8 @@ export function DatePickerWithRange(props: {
|
||||
|
||||
{/* Popover */}
|
||||
<PopoverContent
|
||||
className="w-auto p-0"
|
||||
align={props.popoverAlign || "start"}
|
||||
className="w-auto p-0"
|
||||
sideOffset={10}
|
||||
>
|
||||
<DynamicHeight>
|
||||
@@ -79,19 +78,19 @@ export function DatePickerWithRange(props: {
|
||||
|
||||
<div className={cn("px-4", !props.header && "py-4")}>
|
||||
<TabButtons
|
||||
tabClassName="!text-sm"
|
||||
activeTabClassName="!bg-inverted !text-inverted-foreground"
|
||||
tabClassName="!text-sm"
|
||||
tabContainerClassName="gap-2"
|
||||
tabs={[
|
||||
{
|
||||
isActive: screen === "from",
|
||||
name: "From",
|
||||
onClick: () => setScreen("from"),
|
||||
isActive: screen === "from",
|
||||
},
|
||||
{
|
||||
isActive: screen === "to",
|
||||
name: "To",
|
||||
onClick: () => setScreen("to"),
|
||||
isActive: screen === "to",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
@@ -99,44 +98,44 @@ export function DatePickerWithRange(props: {
|
||||
|
||||
{screen === "from" && (
|
||||
<Calendar
|
||||
key={from.toString()}
|
||||
mode="range"
|
||||
selected={{
|
||||
from,
|
||||
to,
|
||||
classNames={{
|
||||
day_range_end:
|
||||
"!bg-inverted/50 !text-inverted-foreground pointer-events-none",
|
||||
day_range_start: "!bg-inverted !text-inverted-foreground",
|
||||
}}
|
||||
defaultMonth={from}
|
||||
key={from.toString()}
|
||||
mode="range"
|
||||
onDayClick={(newFrom) => {
|
||||
if (isBefore(newFrom, to)) {
|
||||
setFrom(newFrom);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
day_range_start: "!bg-inverted !text-inverted-foreground",
|
||||
day_range_end:
|
||||
"!bg-inverted/50 !text-inverted-foreground pointer-events-none",
|
||||
selected={{
|
||||
from,
|
||||
to,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{screen === "to" && (
|
||||
<Calendar
|
||||
key={to.toString()}
|
||||
mode="range"
|
||||
selected={{
|
||||
from,
|
||||
to,
|
||||
classNames={{
|
||||
day_range_end: "!bg-inverted !text-inverted-foreground",
|
||||
day_range_start:
|
||||
"!bg-inverted/50 !text-inverted-foreground pointer-events-none",
|
||||
}}
|
||||
defaultMonth={to}
|
||||
key={to.toString()}
|
||||
mode="range"
|
||||
onDayClick={(newTo) => {
|
||||
if (isBefore(from, newTo)) {
|
||||
setTo(newTo);
|
||||
}
|
||||
}}
|
||||
classNames={{
|
||||
day_range_end: "!bg-inverted !text-inverted-foreground",
|
||||
day_range_start:
|
||||
"!bg-inverted/50 !text-inverted-foreground pointer-events-none",
|
||||
selected={{
|
||||
from,
|
||||
to,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user