Compare commits
176
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfe9753be6 | ||
|
|
eeb993df75 | ||
|
|
367a270bba | ||
|
|
e91581aec6 | ||
|
|
985c4b0804 | ||
|
|
9fb5401a93 | ||
|
|
e54867550e | ||
|
|
514647cb53 | ||
|
|
eb8638e0c2 | ||
|
|
a6f85ac949 | ||
|
|
726bbde9ca | ||
|
|
025c581be6 | ||
|
|
522968a505 | ||
|
|
454f063f98 | ||
|
|
78cbc412ce | ||
|
|
79c50cb311 | ||
|
|
764ee8ca47 | ||
|
|
783d84467d | ||
|
|
687a733d4a | ||
|
|
96daa2654c | ||
|
|
a2ce5f56ab | ||
|
|
6beec7a35e | ||
|
|
31f371d065 | ||
|
|
fd8674a8a4 | ||
|
|
d3b0dc6646 | ||
|
|
98c5fd0b79 | ||
|
|
2f11259ccd | ||
|
|
97c09003dc | ||
|
|
5d0505c43c | ||
|
|
e552fda9e5 | ||
|
|
cddeda4280 | ||
|
|
cb72f67c92 | ||
|
|
222964dcbb | ||
|
|
ec37c74199 | ||
|
|
2ac1e86b8f | ||
|
|
bdc5964054 | ||
|
|
5fc07f7111 | ||
|
|
80729e9748 | ||
|
|
8832f87f4a | ||
|
|
39a43903ad | ||
|
|
6396729839 | ||
|
|
30fd225538 | ||
|
|
931bbd4952 | ||
|
|
3202ce105c | ||
|
|
36aa3f0937 | ||
|
|
ab43d9426a | ||
|
|
29938abfe3 | ||
|
|
5bb5a437cd | ||
|
|
8b681729a1 | ||
|
|
6c26012a71 | ||
|
|
90b5495efb | ||
|
|
71575afef3 | ||
|
|
70adb57293 | ||
|
|
96fa4102ba | ||
|
|
fc56142880 | ||
|
|
842baa0513 | ||
|
|
601958d5b7 | ||
|
|
bec828de86 | ||
|
|
a576a5b858 | ||
|
|
5e3be734a3 | ||
|
|
ce6d873ccc | ||
|
|
bbe75b0bc8 | ||
|
|
cefa4eb097 | ||
|
|
63ceb2874c | ||
|
|
509c7981ee | ||
|
|
8ea9420cc4 | ||
|
|
8dda1139dc | ||
|
|
a637cab051 | ||
|
|
c31f919cd4 | ||
|
|
ffcd199381 | ||
|
|
b1d78b1f42 | ||
|
|
bc348ea953 | ||
|
|
43a24e2ed4 | ||
|
|
7b40f2e8e4 | ||
|
|
c1d94aec4f | ||
|
|
ebd5908d4a | ||
|
|
21b032d1b8 | ||
|
|
d0616c4486 | ||
|
|
88db5eaeab | ||
|
|
09b1c28c09 | ||
|
|
98f2b56c19 | ||
|
|
500970e001 | ||
|
|
9c58c08945 | ||
|
|
587804d5a8 | ||
|
|
3f83a377bc | ||
|
|
5ab3a81f79 | ||
|
|
931569b374 | ||
|
|
d76618fc00 | ||
|
|
46acf33b5a | ||
|
|
10a84d2810 | ||
|
|
3e8c074011 | ||
|
|
206340cc1d | ||
|
|
6153d57c34 | ||
|
|
39aeb140d2 | ||
|
|
8e9d13c95c | ||
|
|
d35f0b2cc6 | ||
|
|
2f010fa3fc | ||
|
|
944d56f6d8 | ||
|
|
1f1f99ddd0 | ||
|
|
de63195d43 | ||
|
|
e2b3484764 | ||
|
|
c0d81dcaca | ||
|
|
0aea48e37d | ||
|
|
f91e32f133 | ||
|
|
e38400195f | ||
|
|
e02d87b831 | ||
|
|
d5312304f7 | ||
|
|
b80b61b0a6 | ||
|
|
d37e3fe6e3 | ||
|
|
3b91dad963 | ||
|
|
daf5fdafd5 | ||
|
|
399ba2efd0 | ||
|
|
57a0e271b3 | ||
|
|
2d34771e18 | ||
|
|
a2b4daafcf | ||
|
|
b85fe2a59e | ||
|
|
5293fba07e | ||
|
|
294ec4dd0d | ||
|
|
5a4ef087ef | ||
|
|
fc2ecdfd98 | ||
|
|
33936b3a86 | ||
|
|
1133ccc239 | ||
|
|
3eef910a24 | ||
|
|
3d52dd4587 | ||
|
|
24ff6f2ab6 | ||
|
|
e9ae98084a | ||
|
|
d936d4e5b9 | ||
|
|
bc7288370c | ||
|
|
0a4e44575d | ||
|
|
f2307fde02 | ||
|
|
a4dac95630 | ||
|
|
f424d0ab2d | ||
|
|
915442e1c4 | ||
|
|
89728c6578 | ||
|
|
2707db136e | ||
|
|
ca23a95bfd | ||
|
|
997bfa84cc | ||
|
|
824b1bdb69 | ||
|
|
6e3669d384 | ||
|
|
09eb0e6a3f | ||
|
|
786ef9d777 | ||
|
|
f8e69989a3 | ||
|
|
07d191bd8b | ||
|
|
f8d3578bbc | ||
|
|
60a77e0b31 | ||
|
|
eb79810afa | ||
|
|
200e8da84b | ||
|
|
2ece661620 | ||
|
|
fb92f873cd | ||
|
|
63ada206a4 | ||
|
|
7d547a4e33 | ||
|
|
c1008a5314 | ||
|
|
11a833e1dc | ||
|
|
c67d8a9dd2 | ||
|
|
dab7b4ebd8 | ||
|
|
a491d8bd7c | ||
|
|
a7b3d0e72c | ||
|
|
b0337849a5 | ||
|
|
3229e1f03c | ||
|
|
f228e03ecc | ||
|
|
0a7448cb2b | ||
|
|
f51dd8f1ac | ||
|
|
448dc21cba | ||
|
|
2a51d3c5ae | ||
|
|
80a2eba0f4 | ||
|
|
5b48590034 | ||
|
|
8fa6227fe4 | ||
|
|
fbffa718b4 | ||
|
|
9e11c15c2d | ||
|
|
5c23e5237f | ||
|
|
a16293a10c | ||
|
|
7ff46a50d2 | ||
|
|
0c97d7965e | ||
|
|
4a87109289 | ||
|
|
dddf95daca | ||
|
|
06cd6a62d9 |
@@ -11,7 +11,6 @@ packages/thirdweb/src/react/ @joaquim-verges @gregfromstl @MananTank @jnsdls @ed
|
||||
packages/thirdweb/src/reactive/ @joaquim-verges @gregfromstl @MananTank @jnsdls
|
||||
packages/thirdweb/src/gas/ @joaquim-verges @jnsdls
|
||||
packages/thirdweb/src/pay/ @joaquim-verges @gregfromstl @MananTank @jnsdls @edwardysun
|
||||
packages/typedoc-gen/ @MananTank @jnsdls @joaquim-verges
|
||||
packages/service-utils/ @arcoraven @jnsdls @joaquim-verges
|
||||
packages/eslint-config-thirdweb/ @jnsdls @joaquim-verges
|
||||
packages/tw-tsconfig/ @jnsdls @joaquim-verges
|
||||
|
||||
+2
-1
@@ -27,4 +27,5 @@ sitemap*.xml
|
||||
packages/*/typedoc/*
|
||||
|
||||
*storybook.log
|
||||
storybook-static
|
||||
storybook-static
|
||||
.aider*
|
||||
|
||||
@@ -93,4 +93,7 @@ UNTHREAD_GROWTH_TIER_ID=""
|
||||
UNTHREAD_PRO_TIER_ID=""
|
||||
|
||||
# Demo Engine
|
||||
NEXT_PUBLIC_DEMO_ENGINE_URL=""
|
||||
NEXT_PUBLIC_DEMO_ENGINE_URL=""
|
||||
|
||||
# API server secret (required for thirdweb.com SIWE login). Copy from Vercel.
|
||||
API_SERVER_SECRET=""
|
||||
@@ -14,6 +14,11 @@ module.exports = {
|
||||
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:
|
||||
@@ -50,6 +55,13 @@ module.exports = {
|
||||
"FormErrorMessage",
|
||||
"MenuGroup",
|
||||
"MenuItem",
|
||||
"VStack",
|
||||
"HStack",
|
||||
"AspectRatio",
|
||||
"useToast",
|
||||
"useClipboard",
|
||||
"Badge",
|
||||
"Stack",
|
||||
// also the types
|
||||
"ButtonProps",
|
||||
"BadgeProps",
|
||||
@@ -60,6 +72,9 @@ module.exports = {
|
||||
"HelpTextProps",
|
||||
"MenuGroupProps",
|
||||
"MenuItemProps",
|
||||
"AspectRatioProps",
|
||||
"BadgeProps",
|
||||
"StackProps",
|
||||
],
|
||||
message:
|
||||
'Use the equivalent component from "tw-components" instead.',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.1/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.2/schema.json",
|
||||
"extends": ["../../biome.json"],
|
||||
"overrides": [
|
||||
{
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
"next": true,
|
||||
"ignore": ["src/@/components/ui/**"],
|
||||
"project": ["src/**"],
|
||||
"ignoreBinaries": ["only-allow", "knip", "biome"],
|
||||
"ignoreBinaries": ["only-allow", "biome"],
|
||||
"ignoreDependencies": ["@storybook/blocks", "@thirdweb-dev/service-utils"]
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@ const ContentSecurityPolicy = `
|
||||
img-src * data: blob:;
|
||||
media-src * data: blob:;
|
||||
object-src 'none';
|
||||
style-src 'self' 'unsafe-inline' https://vercel.live;
|
||||
font-src 'self' https://vercel.live https://assets.vercel.com;
|
||||
style-src 'self' 'unsafe-inline' vercel.live;
|
||||
font-src 'self' vercel.live assets.vercel.com framerusercontent.com;
|
||||
frame-src * data:;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com;
|
||||
script-src 'self' 'unsafe-eval' 'unsafe-inline' 'wasm-unsafe-eval' 'inline-speculation-rules' *.thirdweb.com *.thirdweb-dev.com vercel.live js.stripe.com framerusercontent.com events.framer.com;
|
||||
connect-src * data: blob:;
|
||||
worker-src 'self' blob:;
|
||||
block-all-mixed-content;
|
||||
@@ -39,6 +39,9 @@ const securityHeaders = [
|
||||
|
||||
const redirects = require("./redirects");
|
||||
|
||||
// add framer paths here
|
||||
const FRAMER_PATHS = ["/connect/sign-in", "/contracts/modular-contracts"];
|
||||
|
||||
/**
|
||||
* @returns {import('next').RemotePattern[]}
|
||||
*/
|
||||
@@ -128,6 +131,10 @@ const moduleExports = {
|
||||
source: "/thirdweb.eth/:path*",
|
||||
destination: "/deployer.thirdweb.eth/:path*",
|
||||
},
|
||||
...FRAMER_PATHS.map((path) => ({
|
||||
source: path,
|
||||
destination: `https://landing.thirdweb.com${path}`,
|
||||
})),
|
||||
];
|
||||
},
|
||||
images: {
|
||||
|
||||
+23
-23
@@ -10,6 +10,7 @@
|
||||
"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",
|
||||
@@ -30,7 +31,6 @@
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@n8tb1t/use-scroll-position": "^2.0.3",
|
||||
"@radix-ui/react-alert-dialog": "^1.1.1",
|
||||
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
||||
"@radix-ui/react-avatar": "^1.1.0",
|
||||
"@radix-ui/react-checkbox": "^1.1.1",
|
||||
"@radix-ui/react-dialog": "1.1.1",
|
||||
@@ -45,8 +45,7 @@
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-tooltip": "1.1.2",
|
||||
"@react-icons/all-files": "^4.1.0",
|
||||
"@sentry/nextjs": "8.30.0",
|
||||
"@sentry/nextjs": "8.32.0",
|
||||
"@shazow/whatsabi": "^0.14.1",
|
||||
"@stripe/react-stripe-js": "^2.7.3",
|
||||
"@stripe/stripe-js": "^3.5.0",
|
||||
@@ -62,13 +61,14 @@
|
||||
"compare-versions": "^6.1.0",
|
||||
"date-fns": "^3.6.0",
|
||||
"flat": "^6.0.1",
|
||||
"framer-motion": "11.5.4",
|
||||
"framer-motion": "11.9.0",
|
||||
"fuse.js": "7.0.0",
|
||||
"input-otp": "^1.2.4",
|
||||
"ioredis": "^5.4.1",
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"lottie-react": "^2.4.0",
|
||||
"lucide-react": "0.441.0",
|
||||
"next": "14.2.12",
|
||||
"next": "14.2.13",
|
||||
"next-plausible": "^3.12.0",
|
||||
"next-seo": "^6.5.0",
|
||||
"next-themes": "^0.3.0",
|
||||
@@ -88,7 +88,6 @@
|
||||
"react-icons": "^5.2.1",
|
||||
"react-intersection-observer": "^9.10.3",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-otp-input": "^3.1.1",
|
||||
"react-responsive-carousel": "^3.2.23",
|
||||
"react-table": "^7.8.0",
|
||||
"recharts": "^2.12.7",
|
||||
@@ -107,24 +106,24 @@
|
||||
"devDependencies": {
|
||||
"@chakra-ui/cli": "^2.4.1",
|
||||
"@chromatic-com/storybook": "2.0.2",
|
||||
"@next/bundle-analyzer": "14.2.12",
|
||||
"@next/eslint-plugin-next": "14.2.12",
|
||||
"@playwright/test": "1.47.1",
|
||||
"@storybook/addon-essentials": "8.3.1",
|
||||
"@storybook/addon-interactions": "8.3.1",
|
||||
"@storybook/addon-links": "8.3.1",
|
||||
"@storybook/addon-onboarding": "8.3.1",
|
||||
"@storybook/addon-viewport": "8.3.1",
|
||||
"@storybook/blocks": "8.3.1",
|
||||
"@storybook/nextjs": "8.3.1",
|
||||
"@storybook/react": "8.3.1",
|
||||
"@storybook/test": "8.3.1",
|
||||
"@next/bundle-analyzer": "14.2.13",
|
||||
"@next/eslint-plugin-next": "14.2.13",
|
||||
"@playwright/test": "1.47.2",
|
||||
"@storybook/addon-essentials": "8.3.4",
|
||||
"@storybook/addon-interactions": "8.3.4",
|
||||
"@storybook/addon-links": "8.3.4",
|
||||
"@storybook/addon-onboarding": "8.3.4",
|
||||
"@storybook/addon-viewport": "8.3.4",
|
||||
"@storybook/blocks": "8.3.4",
|
||||
"@storybook/nextjs": "8.3.4",
|
||||
"@storybook/react": "8.3.4",
|
||||
"@storybook/test": "8.3.4",
|
||||
"@types/color": "^3.0.6",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/papaparse": "^5.3.14",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "^18.3.7",
|
||||
"@types/react": "^18.3.10",
|
||||
"@types/react-dom": "^18",
|
||||
"@types/react-table": "^7.7.20",
|
||||
"@types/spdx-correct": "^3.1.3",
|
||||
@@ -134,13 +133,14 @@
|
||||
"autoprefixer": "^10.4.19",
|
||||
"checkly": "^4.8.1",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-config-biome": "1.8.3",
|
||||
"eslint-config-biome": "1.8.4",
|
||||
"eslint-plugin-react-compiler": "0.0.0-experimental-ca16900-20240916",
|
||||
"eslint-plugin-storybook": "^0.8.0",
|
||||
"eslint-plugin-storybook": "^0.9.0",
|
||||
"knip": "^5.30.6",
|
||||
"next-sitemap": "^4.2.3",
|
||||
"postcss": "8.4.47",
|
||||
"storybook": "8.3.1",
|
||||
"tailwindcss": "3.4.12",
|
||||
"storybook": "8.3.4",
|
||||
"tailwindcss": "3.4.13",
|
||||
"typescript": "5.6.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export type Project = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -34,7 +32,7 @@ export async function getProjects(teamSlug: string) {
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/teams/${teamSlug}/projects`,
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
@@ -59,7 +57,7 @@ export async function getProject(teamSlug: string, projectSlug: string) {
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/teams/${teamSlug}/projects/${projectSlug}`,
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/projects/${projectSlug}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
const TeamAccountRole = {
|
||||
OWNER: "OWNER",
|
||||
MEMBER: "MEMBER",
|
||||
@@ -39,7 +37,7 @@ export async function getMembers(teamSlug: string) {
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/teams/${teamSlug}/members`,
|
||||
`${API_SERVER_URL}/v1/teams/${teamSlug}/members`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import "server-only";
|
||||
import { COOKIE_ACTIVE_ACCOUNT, COOKIE_PREFIX_TOKEN } from "@/constants/cookie";
|
||||
import { API_SERVER_URL } from "@/constants/env";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
const THIRDWEB_API_HOST =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
export type Team = {
|
||||
id: string;
|
||||
name: string;
|
||||
@@ -35,7 +33,7 @@ export async function getTeamBySlug(slug: string) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamRes = await fetch(`${THIRDWEB_API_HOST}/v1/teams/${slug}`, {
|
||||
const teamRes = await fetch(`${API_SERVER_URL}/v1/teams/${slug}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
@@ -57,7 +55,7 @@ export async function getTeams() {
|
||||
return [];
|
||||
}
|
||||
|
||||
const teamsRes = await fetch(`${THIRDWEB_API_HOST}/v1/teams`, {
|
||||
const teamsRes = await fetch(`${API_SERVER_URL}/v1/teams`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { Button } from "../../ui/button";
|
||||
import { GradientAvatar } from "./GradientAvatar";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Avatars/GradientAvatar",
|
||||
component: Story,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 p-10">
|
||||
<p> All images below are set with size-6 className </p>
|
||||
|
||||
<BadgeContainer label="No Src, No id - Skeleton">
|
||||
<GradientAvatar id={undefined} src={undefined} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Src, id=foo - Skeleton">
|
||||
<GradientAvatar id={"foo"} src={undefined} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src, id=foo - Gradient">
|
||||
<GradientAvatar id={"foo"} src={""} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src, id=bar - Gradient">
|
||||
<GradientAvatar id={"bar"} src={""} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Empty/Invalid Src, No id - default fallback">
|
||||
<GradientAvatar src="invalid-src" id={undefined} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<ToggleTest />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleTest() {
|
||||
const [data, setData] = useState<undefined | { src: string; id: string }>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-10 border p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
setData(undefined);
|
||||
} else {
|
||||
setData({
|
||||
id: "foo",
|
||||
src: "https://picsum.photos/400",
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
>
|
||||
Toggle Src
|
||||
</Button>
|
||||
|
||||
<p> Src+ID is: {data ? "set" : "not set"} </p>
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<GradientAvatar src={data?.src} id={data?.id} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<GradientAvatar
|
||||
className="size-6"
|
||||
src={data ? "invalid-src" : undefined}
|
||||
id={undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { useMemo } from "react";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
const gradients = [
|
||||
["#fca5a5", "#b91c1c"],
|
||||
["#fdba74", "#c2410c"],
|
||||
["#fcd34d", "#b45309"],
|
||||
["#fde047", "#a16207"],
|
||||
["#a3e635", "#4d7c0f"],
|
||||
["#86efac", "#15803d"],
|
||||
["#67e8f9", "#0e7490"],
|
||||
["#7dd3fc", "#0369a1"],
|
||||
["#93c5fd", "#1d4ed8"],
|
||||
["#a5b4fc", "#4338ca"],
|
||||
["#c4b5fd", "#6d28d9"],
|
||||
["#d8b4fe", "#7e22ce"],
|
||||
["#f0abfc", "#a21caf"],
|
||||
["#f9a8d4", "#be185d"],
|
||||
["#fda4af", "#be123c"],
|
||||
];
|
||||
|
||||
function getGradientForString(str: string) {
|
||||
const number = Math.abs(
|
||||
str.split("").reduce((acc, b, i) => acc + b.charCodeAt(0) * (i + 1), 0),
|
||||
);
|
||||
const index = number % gradients.length;
|
||||
return gradients[index];
|
||||
}
|
||||
|
||||
export function GradientAvatar(props: {
|
||||
src: string | undefined;
|
||||
id: string | undefined;
|
||||
className: string;
|
||||
}) {
|
||||
const gradient = useMemo(() => {
|
||||
if (!props.id) {
|
||||
return undefined;
|
||||
}
|
||||
return getGradientForString(props.id);
|
||||
}, [props.id]);
|
||||
|
||||
return (
|
||||
<Img
|
||||
src={props.src}
|
||||
className={cn("rounded-full", props.className)}
|
||||
fallback={
|
||||
gradient ? (
|
||||
<div
|
||||
style={{
|
||||
background: `linear-gradient(45deg, ${gradient[0]} 0%, ${gradient[1]} 100%)`,
|
||||
}}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useState } from "react";
|
||||
import { BadgeContainer } from "../../../../stories/utils";
|
||||
import { Button } from "../../ui/button";
|
||||
import { ProjectAvatar } from "./ProjectAvatar";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Avatars/ProjectAvatar",
|
||||
component: Story,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 p-10">
|
||||
<p> All images below are set with size-6 className </p>
|
||||
|
||||
<BadgeContainer label="No Src - Skeleton">
|
||||
<ProjectAvatar src={undefined} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Invalid/Empty Src - BoxIcon Fallback">
|
||||
<ProjectAvatar src={""} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<ToggleTest />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleTest() {
|
||||
const [data, setData] = useState<undefined | { src: string; name: string }>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-10 border p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (data) {
|
||||
setData(undefined);
|
||||
} else {
|
||||
setData({
|
||||
name: "foo",
|
||||
src: "https://picsum.photos/400",
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
>
|
||||
Toggle Src
|
||||
</Button>
|
||||
|
||||
<p> Src+Name is: {data ? "set" : "not set"} </p>
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<ProjectAvatar src={data?.src} className="size-6" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<ProjectAvatar
|
||||
src={data ? "invalid-src" : undefined}
|
||||
className="size-6"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { BoxIcon } from "lucide-react";
|
||||
import { cn } from "../../../lib/utils";
|
||||
|
||||
export function ProjectAvatar(props: {
|
||||
src: string | undefined;
|
||||
className: string | undefined;
|
||||
}) {
|
||||
return (
|
||||
<Img
|
||||
src={props.src}
|
||||
className={cn("rounded-lg border", props.className)}
|
||||
alt={""}
|
||||
fallback={
|
||||
<div className="flex items-center justify-center bg-muted/50">
|
||||
<BoxIcon className="size-[50%] text-muted-foreground" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -28,15 +28,15 @@ export const Mobile: Story = {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground py-4">
|
||||
<div className="lg:p-10 container max-w-[1000px] flex flex-col gap-8">
|
||||
<div className="min-h-screen bg-background py-4 text-foreground">
|
||||
<div className="container flex max-w-[1000px] flex-col gap-8 lg:p-10">
|
||||
<BadgeContainer label="Base">
|
||||
<DangerSettingCard
|
||||
title="This is a title"
|
||||
description="This is a description"
|
||||
buttonLabel="Some Action"
|
||||
buttonOnClick={() => {}}
|
||||
isLoading={false}
|
||||
isPending={false}
|
||||
confirmationDialog={{
|
||||
title: "This is confirmation title",
|
||||
description: "This is confirmation description",
|
||||
@@ -50,7 +50,7 @@ function Story() {
|
||||
description="This is a description"
|
||||
buttonLabel="Some Action"
|
||||
buttonOnClick={() => {}}
|
||||
isLoading={true}
|
||||
isPending={true}
|
||||
confirmationDialog={{
|
||||
title: "This is confirmation title",
|
||||
description: "This is confirmation description",
|
||||
|
||||
@@ -17,31 +17,31 @@ export function DangerSettingCard(props: {
|
||||
description: string;
|
||||
buttonLabel: string;
|
||||
buttonOnClick: () => void;
|
||||
isLoading: boolean;
|
||||
isPending: boolean;
|
||||
confirmationDialog: {
|
||||
title: string;
|
||||
description: string;
|
||||
};
|
||||
}) {
|
||||
return (
|
||||
<div className="border border-red-500/70 rounded-lg overflow-hidden">
|
||||
<div className="px-4 lg:px-6 py-6 bg-muted/50">
|
||||
<h3 className="text-xl font-semibold tracking-tight">{props.title}</h3>
|
||||
<div className="overflow-hidden rounded-lg border border-red-500/70">
|
||||
<div className="bg-muted/50 px-4 py-6 lg:px-6">
|
||||
<h3 className="font-semibold text-xl tracking-tight">{props.title}</h3>
|
||||
|
||||
<p className="text-foreground text-sm mt-1.5 mb-4">
|
||||
<p className="mt-1.5 mb-4 text-foreground text-sm">
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-red-100 dark:bg-red-500/20 px-4 lg:px-6 py-4 flex justify-end border-red-500/70 border-t">
|
||||
<div className="flex justify-end border-red-500/70 border-t bg-red-100 px-4 py-4 lg:px-6 dark:bg-red-500/20">
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="bg-red-600 hover:bg-red-600/80 text-white font-semibold gap-2"
|
||||
disabled={props.isLoading}
|
||||
className="gap-2 bg-red-600 font-semibold text-white hover:bg-red-600/80"
|
||||
disabled={props.isPending}
|
||||
>
|
||||
{props.isLoading && <Spinner className="size-3" />}
|
||||
{props.isPending && <Spinner className="size-3" />}
|
||||
{props.buttonLabel}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -64,11 +64,11 @@ export function DangerSettingCard(props: {
|
||||
|
||||
<Button
|
||||
variant="destructive"
|
||||
className="bg-red-600 hover:bg-red-600/80 text-white font-semibold gap-2"
|
||||
className="gap-2 bg-red-600 font-semibold text-white hover:bg-red-600/80"
|
||||
onClick={props.buttonOnClick}
|
||||
disabled={props.isLoading}
|
||||
disabled={props.isPending}
|
||||
>
|
||||
{props.isLoading && <Spinner className="size-3" />}
|
||||
{props.isPending && <Spinner className="size-3" />}
|
||||
{props.buttonLabel}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ExportToCSVButton(props: {
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={props.disabled}
|
||||
className={cn("border flex gap-2 items-center text-xs", props.className)}
|
||||
className={cn("flex items-center gap-2 border text-xs", props.className)}
|
||||
onClick={async () => {
|
||||
exportMutation.mutate();
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { AsteriskIcon, InfoIcon } from "lucide-react";
|
||||
|
||||
export function FormFieldSetup(props: {
|
||||
htmlFor: string;
|
||||
label: string;
|
||||
errorMessage: React.ReactNode | undefined;
|
||||
children: React.ReactNode;
|
||||
tooltip: React.ReactNode | undefined;
|
||||
isRequired: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 inline-flex items-center gap-1">
|
||||
<Label htmlFor={props.htmlFor}>{props.label}</Label>
|
||||
|
||||
{props.isRequired && (
|
||||
<AsteriskIcon className="size-3.5 text-destructive-text" />
|
||||
)}
|
||||
|
||||
{props.tooltip && (
|
||||
<ToolTipLabel label={props.tooltip}>
|
||||
<InfoIcon className="size-3.5 text-muted-foreground" />
|
||||
</ToolTipLabel>
|
||||
)}
|
||||
</div>
|
||||
{props.children}
|
||||
{props.errorMessage && (
|
||||
<p className="mt-1 text-destructive-text text-sm">
|
||||
{props.errorMessage}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
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 { Img } from "./Img";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/Img",
|
||||
component: Story,
|
||||
parameters: {},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="flex flex-col gap-10 p-10">
|
||||
<p> All images below are set with size-20 className </p>
|
||||
|
||||
<BadgeContainer label="No Src - pending">
|
||||
<Img className="size-20" src={undefined} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Failed to load - fallback">
|
||||
<Img className="size-20" src="invalid-src" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="No Src, pending - rounded-full">
|
||||
<Img className="size-20 rounded-full" src={undefined} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Failed to load - fallback - rounded-full">
|
||||
<Img className="size-20 rounded-full" src="invalid-src" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<Img className="size-20" src="https://picsum.photos/200" />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Valid Src, rounded">
|
||||
<Img className="size-20 rounded-full" src="https://picsum.photos/200" />
|
||||
</BadgeContainer>
|
||||
|
||||
<ToggleTest />
|
||||
|
||||
<BadgeContainer label="Custom Skeleton">
|
||||
<Img
|
||||
src={undefined}
|
||||
skeleton={
|
||||
<div className="flex items-center justify-center rounded-lg border">
|
||||
<Spinner className="size-6" />
|
||||
</div>
|
||||
}
|
||||
className="size-20"
|
||||
/>
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="Custom Fallback">
|
||||
<Img
|
||||
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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ToggleTest() {
|
||||
const [src, setSrc] = useState<undefined | string>(undefined);
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col gap-10 border p-6">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (src) {
|
||||
setSrc(undefined);
|
||||
} else {
|
||||
setSrc("https://picsum.photos/400");
|
||||
}
|
||||
}}
|
||||
className="absolute top-6 right-6 inline-flex"
|
||||
>
|
||||
Toggle Src
|
||||
</Button>
|
||||
|
||||
<p> Src is {src ? "set" : "not set"} </p>
|
||||
|
||||
<BadgeContainer label="Valid Src">
|
||||
<Img className="size-20" src={src} />
|
||||
</BadgeContainer>
|
||||
|
||||
<BadgeContainer label="invalid Src">
|
||||
<Img
|
||||
className="size-20 rounded-full"
|
||||
src={src ? "invalid-src" : undefined}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
type imgElementProps = React.DetailedHTMLProps<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
HTMLImageElement
|
||||
> & {
|
||||
skeleton?: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
src: string | undefined;
|
||||
};
|
||||
|
||||
export function Img(props: imgElementProps) {
|
||||
const [_status, setStatus] = useState<"pending" | "fallback" | "loaded">(
|
||||
"pending",
|
||||
);
|
||||
const status =
|
||||
props.src === undefined
|
||||
? "pending"
|
||||
: props.src === ""
|
||||
? "fallback"
|
||||
: _status;
|
||||
const { className, fallback, skeleton, ...restProps } = props;
|
||||
const defaultSkeleton = <div className="animate-pulse bg-accent" />;
|
||||
const defaultFallback = <div className="bg-muted" />;
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<img
|
||||
{...restProps}
|
||||
onLoad={() => {
|
||||
setStatus("loaded");
|
||||
}}
|
||||
onError={() => {
|
||||
setStatus("fallback");
|
||||
}}
|
||||
style={{
|
||||
opacity: status === "loaded" ? 1 : 0,
|
||||
}}
|
||||
alt={restProps.alt || ""}
|
||||
className={cn(
|
||||
"fade-in-0 object-cover transition-opacity duration-300",
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
|
||||
{status !== "loaded" && (
|
||||
<div
|
||||
className={cn(
|
||||
"fade-in-0 absolute inset-0 overflow-hidden transition-opacity duration-300 [&>*]:h-full [&>*]:w-full",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{status === "pending" && (skeleton || defaultSkeleton)}
|
||||
{status === "fallback" && (fallback || defaultFallback)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,30 +4,50 @@ 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 { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
import type { SidebarLink } from "./Sidebar";
|
||||
import {
|
||||
RenderSidebarLinks,
|
||||
type SidebarBaseLink,
|
||||
type SidebarLink,
|
||||
} from "./Sidebar";
|
||||
|
||||
export function MobileSidebar(props: {
|
||||
links?: SidebarLink[];
|
||||
links: SidebarLink[];
|
||||
footer?: React.ReactNode;
|
||||
trigger?: React.ReactNode;
|
||||
triggerClassName?: string;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const pathname = usePathname();
|
||||
const activeLink = props.links?.find((link) => {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
|
||||
const activeLink = useMemo(() => {
|
||||
function isActive(link: SidebarBaseLink) {
|
||||
if (link.exactMatch) {
|
||||
return link.href === pathname;
|
||||
}
|
||||
return pathname?.startsWith(link.href);
|
||||
}
|
||||
return pathname?.startsWith(link.href);
|
||||
});
|
||||
|
||||
for (const link of props.links) {
|
||||
if ("group" in link) {
|
||||
for (const subLink of link.links) {
|
||||
if (isActive(subLink)) {
|
||||
return subLink;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isActive(link)) {
|
||||
return link;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [props.links, pathname]);
|
||||
|
||||
const defaultTrigger = (
|
||||
<Button
|
||||
className={cn(
|
||||
"w-full lg:hidden text-left justify-between gap-2 bg-muted/50",
|
||||
"w-full justify-between gap-2 bg-muted/50 text-left lg:hidden",
|
||||
props.triggerClassName,
|
||||
)}
|
||||
variant="outline"
|
||||
@@ -41,7 +61,7 @@ export function MobileSidebar(props: {
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>{props.trigger || defaultTrigger}</DialogTrigger>
|
||||
<DialogContent
|
||||
className="p-4 rounded-t-xl rounded-b-none"
|
||||
className="no-scrollbar max-h-[80vh] overflow-auto rounded-t-xl rounded-b-none p-4"
|
||||
dialogCloseClassName="hidden"
|
||||
onClick={(e) => {
|
||||
if (e.target instanceof HTMLAnchorElement) {
|
||||
@@ -49,23 +69,7 @@ export function MobileSidebar(props: {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
{props.links?.map((link) => {
|
||||
return (
|
||||
<Button size="sm" variant="ghost" asChild key={link.href}>
|
||||
<NavLink
|
||||
href={link.href}
|
||||
className="!text-left justify-start flex gap-2 h-auto py-3"
|
||||
activeClassName="bg-accent"
|
||||
exactMatch={link.exactMatch}
|
||||
tracking={link.tracking}
|
||||
>
|
||||
{link.label}
|
||||
</NavLink>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<RenderSidebarLinks links={props.links} />
|
||||
{props.footer}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@@ -28,8 +28,8 @@ export const Mobile: Story = {
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background text-foreground py-6">
|
||||
<div className="lg:p-10 container max-w-[1100px] flex flex-col gap-10">
|
||||
<div className="min-h-screen bg-background py-6 text-foreground">
|
||||
<div className="container flex max-w-[1100px] flex-col gap-10 lg:p-10">
|
||||
<BadgeContainer label="No Header">
|
||||
<SettingsCard
|
||||
bottomText="This is bottom text"
|
||||
@@ -81,10 +81,10 @@ function Story() {
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={"You do not have permission to edit this"}
|
||||
noPermissionText="You do not have permission to edit this"
|
||||
errorText={undefined}
|
||||
>
|
||||
<ChildrenPlaceholder />
|
||||
@@ -100,7 +100,7 @@ function Story() {
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
@@ -119,7 +119,7 @@ function Story() {
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: true,
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
@@ -138,7 +138,7 @@ function Story() {
|
||||
}}
|
||||
saveButton={{
|
||||
disabled: false,
|
||||
isLoading: true,
|
||||
isPending: true,
|
||||
onClick: () => {},
|
||||
}}
|
||||
noPermissionText={undefined}
|
||||
@@ -154,8 +154,8 @@ function Story() {
|
||||
|
||||
function ChildrenPlaceholder() {
|
||||
return (
|
||||
<div className="h-10 flex items-center justify-center text-muted-foreground/50 text-xs">
|
||||
{"CHILDREN GOES HERE"}
|
||||
<div className="flex h-10 items-center justify-center text-muted-foreground/50 text-xs">
|
||||
CHILDREN GOES HERE
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,37 +3,38 @@ import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type React from "react";
|
||||
|
||||
export function SettingsCard(props: {
|
||||
bottomText: React.ReactNode;
|
||||
header?: {
|
||||
description: string | undefined;
|
||||
title: string;
|
||||
};
|
||||
children: React.ReactNode;
|
||||
errorText: string | undefined;
|
||||
noPermissionText: string | undefined;
|
||||
saveButton?: {
|
||||
onClick?: () => void;
|
||||
disabled: boolean;
|
||||
isLoading: boolean;
|
||||
type?: "submit";
|
||||
};
|
||||
}) {
|
||||
export function SettingsCard(
|
||||
props: React.PropsWithChildren<{
|
||||
bottomText: React.ReactNode;
|
||||
header?: {
|
||||
description: string | undefined;
|
||||
title: string;
|
||||
};
|
||||
errorText: string | undefined;
|
||||
noPermissionText: string | undefined;
|
||||
saveButton?: {
|
||||
onClick?: () => void;
|
||||
disabled: boolean;
|
||||
isPending: boolean;
|
||||
type?: "submit";
|
||||
};
|
||||
}>,
|
||||
) {
|
||||
return (
|
||||
<div className="border border-border rounded-lg bg-muted/50 relative">
|
||||
<div className="relative rounded-lg border border-border bg-muted/50">
|
||||
<div
|
||||
className={cn(
|
||||
"border-b border-border px-4 lg:px-6 py-6 relative",
|
||||
"relative border-border border-b px-4 py-6 lg:px-6",
|
||||
props.noPermissionText && "cursor-not-allowed",
|
||||
)}
|
||||
>
|
||||
{props.header && (
|
||||
<>
|
||||
<h3 className="text-xl font-semibold tracking-tight">
|
||||
<h3 className="font-semibold text-xl tracking-tight">
|
||||
{props.header.title}
|
||||
</h3>
|
||||
{props.header.description && (
|
||||
<p className="text-foreground text-sm mt-1.5 mb-4">
|
||||
<p className="mt-1.5 mb-4 text-foreground text-sm">
|
||||
{props.header.description}
|
||||
</p>
|
||||
)}
|
||||
@@ -43,7 +44,7 @@ export function SettingsCard(props: {
|
||||
{props.children}
|
||||
</div>
|
||||
|
||||
<div className="px-4 lg:px-6 py-3 flex items-center justify-between gap-2 min-h-[60px]">
|
||||
<div className="flex min-h-[60px] items-center justify-between gap-2 px-4 py-3 lg:px-6">
|
||||
{props.noPermissionText ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{props.noPermissionText}
|
||||
@@ -59,12 +60,12 @@ export function SettingsCard(props: {
|
||||
size="sm"
|
||||
className="gap-2"
|
||||
onClick={props.saveButton.onClick}
|
||||
disabled={props.saveButton.disabled || props.saveButton.isLoading}
|
||||
disabled={props.saveButton.disabled || props.saveButton.isPending}
|
||||
variant="outline"
|
||||
type={props.saveButton.type}
|
||||
>
|
||||
{props.saveButton.isLoading && <Spinner className="size-3" />}
|
||||
{props.saveButton.isLoading ? "Saving" : "Save"}
|
||||
{props.saveButton.isPending && <Spinner className="size-3" />}
|
||||
{props.saveButton.isPending ? "Saving" : "Save"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type React from "react";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { NavLink } from "../ui/NavLink";
|
||||
|
||||
export type SidebarLink = {
|
||||
export type SidebarBaseLink = {
|
||||
href: string;
|
||||
label: React.ReactNode;
|
||||
exactMatch?: boolean;
|
||||
@@ -14,6 +14,13 @@ export type SidebarLink = {
|
||||
};
|
||||
};
|
||||
|
||||
export type SidebarLink =
|
||||
| SidebarBaseLink
|
||||
| {
|
||||
group: string;
|
||||
links: SidebarBaseLink[];
|
||||
};
|
||||
|
||||
type SidebarContentProps = {
|
||||
header?: React.ReactNode;
|
||||
links: SidebarLink[];
|
||||
@@ -24,30 +31,49 @@ export function Sidebar(props: SidebarContentProps) {
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
"w-[230px] flex-shrink-0 hidden lg:block self-start sticky top-0",
|
||||
"sticky top-0 hidden w-[230px] flex-shrink-0 self-start lg:block",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="pt-7">
|
||||
{props.header}
|
||||
<div className="flex flex-col gap-1">
|
||||
{props.links?.map((link) => {
|
||||
const isExternal = link.href.startsWith("http");
|
||||
return (
|
||||
<NavLink
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="py-2 hover:bg-muted rounded-md px-3 text-muted-foreground text-sm flex items-center gap-2"
|
||||
activeClassName="text-foreground"
|
||||
exactMatch={link.exactMatch}
|
||||
>
|
||||
{link.label}
|
||||
{isExternal && <ExternalLinkIcon className="size-3" />}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
<RenderSidebarLinks links={props.links} />
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
export function RenderSidebarLinks(props: { links: SidebarLink[] }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-1">
|
||||
{props.links.map((link, i) => {
|
||||
if ("group" in link) {
|
||||
return (
|
||||
<div className={cn({ "mt-6": i !== 0 })}>
|
||||
<p className={cn("px-3 py-2 font-medium text-foreground")}>
|
||||
{link.group}
|
||||
</p>
|
||||
<RenderSidebarLinks links={link.links} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const isExternal = link.href.startsWith("http");
|
||||
return (
|
||||
<NavLink
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
className="flex items-center gap-2 rounded-md px-3 py-2 text-muted-foreground text-sm hover:bg-muted"
|
||||
activeClassName="text-foreground"
|
||||
exactMatch={link.exactMatch}
|
||||
>
|
||||
{link.label}
|
||||
{isExternal && <ExternalLinkIcon className="size-3" />}
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -55,8 +55,8 @@ function Story() {
|
||||
|
||||
function ContentPlaceholder() {
|
||||
return (
|
||||
<div className="bg-muted grow flex items-center justify-center">
|
||||
<p className="text-muted-foreground text-3xl"> CHILDREN </p>
|
||||
<div className="flex grow items-center justify-center bg-muted">
|
||||
<p className="text-3xl text-muted-foreground"> CHILDREN </p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function SidebarLayout(props: {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col lg:flex-row gap-6 container px-4 w-full grow max-sm:pt-6",
|
||||
"container flex w-full grow flex-col gap-6 px-4 max-sm:pt-6 lg:flex-row",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -23,7 +23,7 @@ export function SidebarLayout(props: {
|
||||
triggerClassName={props.mobileSidebarClassName}
|
||||
/>
|
||||
{/* min-w-0 is enough to ensure the content inside does not stretch this container */}
|
||||
<main className="grow max-sm:w-full flex flex-col lg:pt-8 pb-10 min-w-0">
|
||||
<main className="flex min-w-0 grow flex-col pb-10 max-sm:w-full lg:pt-8">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "app/components/ThirdwebMiniLogo";
|
||||
import { DiscordIcon } from "components/icons/brand-icons/DiscordIcon";
|
||||
import { GithubIcon } from "components/icons/brand-icons/GithubIcon";
|
||||
import { InstagramIcon } from "components/icons/brand-icons/InstagramIcon";
|
||||
import { LinkedInIcon } from "components/icons/brand-icons/LinkedinIcon";
|
||||
import { RedditIcon } from "components/icons/brand-icons/RedditIcon";
|
||||
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 {
|
||||
SiDiscord,
|
||||
SiGithub,
|
||||
SiInstagram,
|
||||
SiLinkedin,
|
||||
SiReddit,
|
||||
SiTiktok,
|
||||
SiX,
|
||||
SiYoutube,
|
||||
} from "react-icons/si";
|
||||
|
||||
type AppFooterProps = {
|
||||
className?: string;
|
||||
@@ -21,31 +19,31 @@ export function AppFooter(props: AppFooterProps) {
|
||||
return (
|
||||
<footer
|
||||
className={cn(
|
||||
"bg-muted/50 border-t border-border py-6 md:py-8 w-full",
|
||||
"w-full border-border border-t bg-muted/50 py-6 md:py-8",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="container flex flex-col gap-4 md:gap-6">
|
||||
{/* top row */}
|
||||
<div className="flex flex-col md:flex-row gap-4 md:justify-between md:items-center">
|
||||
<div className="flex flex-row gap-3 items-center">
|
||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||
<div className="flex flex-row items-center gap-3">
|
||||
<ThirdwebMiniLogo className="h-7" />
|
||||
<p className="text-sm text-muted-foreground">© 2024 thirdweb</p>
|
||||
<p className="text-muted-foreground text-sm">© 2024 thirdweb</p>
|
||||
</div>
|
||||
<div className="flex flex-row gap-3">
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://github.com/thirdweb-dev" target="_blank">
|
||||
<SiGithub className="text-muted-foreground size-5" />
|
||||
<GithubIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://www.tiktok.com/@thirdweb" target="_blank">
|
||||
<SiTiktok className="text-muted-foreground size-5" />
|
||||
<TiktokIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://www.instagram.com/thirdweb/" target="_blank">
|
||||
<SiInstagram className="text-muted-foreground size-5" />
|
||||
<InstagramIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
@@ -53,83 +51,83 @@ export function AppFooter(props: AppFooterProps) {
|
||||
href="hhttps://www.linkedin.com/company/third-web/"
|
||||
target="_blank"
|
||||
>
|
||||
<SiLinkedin className="text-muted-foreground size-5" />
|
||||
<LinkedInIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://www.youtube.com/@thirdweb_" target="_blank">
|
||||
<SiYoutube className="text-muted-foreground size-5" />
|
||||
<YoutubeIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://discord.gg/thirdweb" target="_blank">
|
||||
<SiDiscord className="text-muted-foreground size-5" />
|
||||
<DiscordIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://www.reddit.com/r/thirdweb/" target="_blank">
|
||||
<SiReddit className="text-muted-foreground size-5" />
|
||||
<RedditIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
<Button size="icon" variant="ghost" asChild className="size-9">
|
||||
<Link href="https://x.com/thirdweb" target="_blank">
|
||||
<SiX className="text-muted-foreground size-5" />
|
||||
<XIcon className="size-5 text-muted-foreground" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/* bottom row */}
|
||||
<div className="grid grid-rows-5 grid-cols-2 grid-flow-col gap-2 md:flex md:flex-row md:justify-between">
|
||||
<div className="grid grid-flow-col grid-cols-2 grid-rows-5 gap-2 md:flex md:flex-row md:justify-between">
|
||||
<Link
|
||||
className="py-[6px] px-[10px] text-muted-foreground text-sm hover:underline"
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="/"
|
||||
>
|
||||
Home
|
||||
</Link>
|
||||
<Link
|
||||
className="py-[6px] px-[10px] text-muted-foreground text-sm hover:underline"
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://blog.thirdweb.com"
|
||||
target="_blank"
|
||||
>
|
||||
Blog
|
||||
</Link>
|
||||
<Link
|
||||
className="py-[6px] px-[10px] text-muted-foreground text-sm hover:underline"
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://portal.thirdweb.com/changelog"
|
||||
target="_blank"
|
||||
>
|
||||
Changelog
|
||||
</Link>
|
||||
<Link
|
||||
className="py-[6px] px-[10px] text-muted-foreground text-sm hover:underline"
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://feedback.thirdweb.com/"
|
||||
target="_blank"
|
||||
>
|
||||
Feedback
|
||||
</Link>
|
||||
<Link
|
||||
className="py-[6px] px-[10px] text-muted-foreground text-sm hover:underline"
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://thirdweb.com/privacy"
|
||||
target="_blank"
|
||||
>
|
||||
Privacy Policy
|
||||
</Link>
|
||||
<Link
|
||||
className="py-[6px] px-[10px] text-muted-foreground text-sm hover:underline"
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://thirdweb.com/tos"
|
||||
target="_blank"
|
||||
>
|
||||
Terms of Service
|
||||
</Link>
|
||||
<Link
|
||||
className="py-[6px] px-[10px] text-muted-foreground text-sm hover:underline"
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://thirdweb.com/gas"
|
||||
target="_blank"
|
||||
>
|
||||
Gas Estimator
|
||||
</Link>
|
||||
<Link
|
||||
className="py-[6px] px-[10px] text-muted-foreground text-sm hover:underline"
|
||||
className="px-[10px] py-[6px] text-muted-foreground text-sm hover:underline"
|
||||
href="https://thirdweb.com/chainlist"
|
||||
target="_blank"
|
||||
>
|
||||
|
||||
@@ -1,14 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
type ChartConfig,
|
||||
ChartContainer,
|
||||
@@ -18,11 +9,9 @@ import {
|
||||
ChartTooltipContent,
|
||||
} from "@/components/ui/chart";
|
||||
import { useMemo } from "react";
|
||||
import { Area, AreaChart, CartesianGrid, XAxis } from "recharts";
|
||||
|
||||
type ThirdwebAreaChartProps<TConfig extends ChartConfig> = {
|
||||
// metadata
|
||||
title: string;
|
||||
description?: string;
|
||||
// chart config
|
||||
config: TConfig;
|
||||
data: Array<Record<keyof TConfig, number> & { time: number | string | Date }>;
|
||||
@@ -37,73 +26,65 @@ export function ThirdwebAreaChart<TConfig extends ChartConfig>(
|
||||
) {
|
||||
const configKeys = useMemo(() => Object.keys(props.config), [props.config]);
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>{props.title}</CardTitle>
|
||||
{props.description && (
|
||||
<CardDescription>{props.description}</CardDescription>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={props.data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => new Date(value).toLocaleDateString()}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<linearGradient
|
||||
key={key}
|
||||
id={`fill_${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<div className="rounded-lg border border-border bg-muted/50 px-4 pt-10 pb-4">
|
||||
<ChartContainer config={props.config} className={props.chartClassName}>
|
||||
<AreaChart
|
||||
accessibilityLayer
|
||||
data={props.data}
|
||||
margin={{
|
||||
left: 12,
|
||||
right: 12,
|
||||
}}
|
||||
>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="time"
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={20}
|
||||
tickFormatter={(value) => new Date(value).toLocaleDateString()}
|
||||
/>
|
||||
<ChartTooltip cursor={false} content={<ChartTooltipContent />} />
|
||||
<defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
<linearGradient
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
id={`fill_${key}`}
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="5%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="95%"
|
||||
stopColor={`var(--color-${key})`}
|
||||
stopOpacity={0.1}
|
||||
/>
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
{configKeys.map((key) => (
|
||||
<Area
|
||||
key={key}
|
||||
dataKey={key}
|
||||
type="natural"
|
||||
fill={`url(#fill_${key})`}
|
||||
fillOpacity={0.4}
|
||||
stroke={`var(--color-${key})`}
|
||||
stackId="a"
|
||||
/>
|
||||
))}
|
||||
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
)}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
{props.showLegend && (
|
||||
<ChartLegend content={<ChartLegendContent />} className="pt-8" />
|
||||
)}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { BadgeContainer, mobileViewport } from "../../../stories/utils";
|
||||
import { MultiSelect } from "./multi-select";
|
||||
|
||||
const meta = {
|
||||
title: "blocks/MultiSelect",
|
||||
component: Story,
|
||||
parameters: {
|
||||
nextjs: {
|
||||
appDirectory: true,
|
||||
},
|
||||
},
|
||||
} satisfies Meta<typeof Story>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Desktop: Story = {
|
||||
args: {},
|
||||
};
|
||||
|
||||
export const Mobile: Story = {
|
||||
args: {},
|
||||
parameters: {
|
||||
viewport: mobileViewport("iphone14"),
|
||||
},
|
||||
};
|
||||
|
||||
function createList(len: number) {
|
||||
return Array.from({ length: len }, (_, i) => ({
|
||||
value: `${i}`,
|
||||
label: `Item ${i}`,
|
||||
}));
|
||||
}
|
||||
|
||||
function Story() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-[600px] flex-col gap-6 px-4 py-6">
|
||||
<VariantTest defaultValues={[]} storyLabel="5 items" listLen={5} />
|
||||
<VariantTest defaultValues={[]} storyLabel="5000 items" listLen={5000} />
|
||||
<VariantTest
|
||||
defaultValues={["1", "3", "5"]}
|
||||
storyLabel="20 items, 3 selected by default"
|
||||
listLen={20}
|
||||
/>
|
||||
<VariantTest
|
||||
defaultValues={["1", "2", "3", "4", "5", "6", "7"]}
|
||||
storyLabel="20 items, 7 selected, show max 3"
|
||||
listLen={20}
|
||||
maxCount={3}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VariantTest(props: {
|
||||
defaultValues: string[];
|
||||
maxCount?: number;
|
||||
storyLabel: string;
|
||||
listLen: number;
|
||||
}) {
|
||||
const list = useMemo(() => createList(props.listLen), [props.listLen]);
|
||||
const [values, setValues] = useState<string[]>(props.defaultValues);
|
||||
|
||||
return (
|
||||
<BadgeContainer label={props.storyLabel}>
|
||||
<MultiSelect
|
||||
selectedValues={values}
|
||||
options={list}
|
||||
onSelectedValuesChange={(values) => {
|
||||
setValues(values);
|
||||
}}
|
||||
placeholder="Select items"
|
||||
maxCount={props.maxCount}
|
||||
/>
|
||||
</BadgeContainer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CheckIcon, ChevronDown, SearchIcon, XIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useShowMore } from "../../lib/useShowMore";
|
||||
import { ScrollShadow } from "../ui/ScrollShadow/ScrollShadow";
|
||||
import { Input } from "../ui/input";
|
||||
|
||||
interface MultiSelectProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
}[];
|
||||
|
||||
selectedValues: string[];
|
||||
onSelectedValuesChange: (value: string[]) => void;
|
||||
placeholder: string;
|
||||
|
||||
/**
|
||||
* Maximum number of items to display. Extra selected items will be summarized.
|
||||
* Optional, defaults to 3.
|
||||
*/
|
||||
maxCount?: number;
|
||||
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MultiSelect = React.forwardRef<
|
||||
HTMLButtonElement,
|
||||
MultiSelectProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
options,
|
||||
onSelectedValuesChange,
|
||||
placeholder,
|
||||
maxCount = Number.POSITIVE_INFINITY,
|
||||
className,
|
||||
selectedValues,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = React.useState(false);
|
||||
const [searchValue, setSearchValue] = React.useState("");
|
||||
|
||||
const handleInputKeyDown = React.useCallback(
|
||||
(event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter") {
|
||||
setIsPopoverOpen(true);
|
||||
} else if (event.key === "Backspace" && !event.currentTarget.value) {
|
||||
const newSelectedValues = [...selectedValues];
|
||||
newSelectedValues.pop();
|
||||
onSelectedValuesChange(newSelectedValues);
|
||||
}
|
||||
},
|
||||
[selectedValues, onSelectedValuesChange],
|
||||
);
|
||||
|
||||
const toggleOption = React.useCallback(
|
||||
(option: string) => {
|
||||
const newSelectedValues = selectedValues.includes(option)
|
||||
? selectedValues.filter((value) => value !== option)
|
||||
: [...selectedValues, option];
|
||||
onSelectedValuesChange(newSelectedValues);
|
||||
},
|
||||
[selectedValues, onSelectedValuesChange],
|
||||
);
|
||||
|
||||
const handleClear = React.useCallback(() => {
|
||||
onSelectedValuesChange([]);
|
||||
}, [onSelectedValuesChange]);
|
||||
|
||||
const handleTogglePopover = () => {
|
||||
setIsPopoverOpen((prev) => !prev);
|
||||
};
|
||||
|
||||
const clearExtraOptions = React.useCallback(() => {
|
||||
const newSelectedValues = selectedValues.slice(0, maxCount);
|
||||
onSelectedValuesChange(newSelectedValues);
|
||||
}, [selectedValues, onSelectedValuesChange, maxCount]);
|
||||
|
||||
// show 50 initially and then 20 more when reaching the end
|
||||
const { itemsToShow, lastItemRef } = useShowMore<HTMLButtonElement>(50, 20);
|
||||
|
||||
const optionsToShow = React.useMemo(() => {
|
||||
const searchValLowercase = searchValue.toLowerCase();
|
||||
const filteredOptions = options.filter((option) => {
|
||||
return option.label.toLowerCase().includes(searchValLowercase);
|
||||
});
|
||||
|
||||
return filteredOptions.slice(0, itemsToShow);
|
||||
}, [options, searchValue, itemsToShow]);
|
||||
|
||||
return (
|
||||
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
onClick={handleTogglePopover}
|
||||
className={cn(
|
||||
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border bg-inherit p-3 hover:bg-inherit",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{selectedValues.length > 0 ? (
|
||||
<div className="flex w-full justify-between">
|
||||
{/* badges */}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
{selectedValues.slice(0, maxCount).map((value) => {
|
||||
const option = options.find((o) => o.value === value);
|
||||
return (
|
||||
<ClosableBadge
|
||||
key={value}
|
||||
label={option?.label || ""}
|
||||
onClose={() => toggleOption(value)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* +X more */}
|
||||
{selectedValues.length > maxCount && (
|
||||
<ClosableBadge
|
||||
label={`+ ${selectedValues.length - maxCount} more`}
|
||||
onClose={clearExtraOptions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
{/* Clear All */}
|
||||
{/* biome-ignore lint/a11y/useKeyWithClickEvents: <explanation> */}
|
||||
{/* biome-ignore lint/a11y/useFocusableInteractive: <explanation> */}
|
||||
<div
|
||||
role="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
handleClear();
|
||||
}}
|
||||
className="rounded p-1 hover:bg-muted"
|
||||
>
|
||||
<XIcon className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="flex h-full min-h-6"
|
||||
/>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
{placeholder}
|
||||
</span>
|
||||
<ChevronDown className="h-4 cursor-pointer text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="p-0"
|
||||
align="center"
|
||||
sideOffset={10}
|
||||
onEscapeKeyDown={() => setIsPopoverOpen(false)}
|
||||
style={{
|
||||
width: "var(--radix-popover-trigger-width)",
|
||||
maxHeight: "var(--radix-popover-content-available-height)",
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{/* Search */}
|
||||
<div className="relative">
|
||||
<Input
|
||||
placeholder="Search"
|
||||
value={searchValue}
|
||||
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"
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
<SearchIcon className="-translate-y-1/2 absolute top-1/2 left-4 size-4 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
<ScrollShadow
|
||||
scrollableClassName="max-h-[350px] p-1"
|
||||
className="rounded"
|
||||
>
|
||||
{/* List */}
|
||||
<div>
|
||||
{optionsToShow.length === 0 && (
|
||||
<div className="flex justify-center py-10">
|
||||
No results found
|
||||
</div>
|
||||
)}
|
||||
|
||||
{optionsToShow.map((option, i) => {
|
||||
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"
|
||||
ref={
|
||||
i === optionsToShow.length - 1 ? lastItemRef : undefined
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"flex size-4 items-center justify-center rounded-sm border border-foreground",
|
||||
isSelected
|
||||
? "bg-inverted text-inverted-foreground"
|
||||
: "opacity-50 [&_svg]:invisible",
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="size-4" />
|
||||
</div>
|
||||
|
||||
<span>{option.label}</span>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
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}
|
||||
<XIcon
|
||||
className="h-3 w-3 cursor-pointer text-muted-foreground"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
props.onClose();
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
MultiSelect.displayName = "MultiSelect";
|
||||
@@ -7,8 +7,9 @@ import {
|
||||
HoverCardTrigger,
|
||||
} from "@/components/ui/hover-card";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { Check, Copy, ExternalLinkIcon } from "lucide-react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { type ThirdwebClient, isAddress } from "thirdweb";
|
||||
import { ZERO_ADDRESS } from "thirdweb";
|
||||
import {
|
||||
@@ -45,7 +46,7 @@ export function WalletAddress(props: {
|
||||
client: thirdwebClient,
|
||||
});
|
||||
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const { onCopy, hasCopied } = useClipboard(address, 2000);
|
||||
|
||||
if (!isAddress(address)) {
|
||||
return <span>Invalid Address ({address})</span>;
|
||||
@@ -53,19 +54,9 @@ export function WalletAddress(props: {
|
||||
|
||||
// special case for zero address
|
||||
if (address === ZERO_ADDRESS) {
|
||||
return <span className="font-mono cursor-pointer">{shortenedAddress}</span>;
|
||||
return <span className="cursor-pointer font-mono">{shortenedAddress}</span>;
|
||||
}
|
||||
|
||||
const copyToClipboard = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(address);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 2000); // Reset after 2 seconds
|
||||
} catch (err) {
|
||||
console.error("Failed to copy: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<HoverCard>
|
||||
<HoverCardTrigger asChild>
|
||||
@@ -73,7 +64,7 @@ export function WalletAddress(props: {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
variant="link"
|
||||
className={cn(
|
||||
"flex flex-row gap-2 items-center px-0",
|
||||
"flex flex-row items-center gap-2 px-0",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -84,7 +75,7 @@ export function WalletAddress(props: {
|
||||
thirdwebClient={thirdwebClient}
|
||||
/>
|
||||
)}
|
||||
<span className="font-mono cursor-pointer">
|
||||
<span className="cursor-pointer font-mono">
|
||||
{profiles.data?.[0]?.name || shortenedAddress}
|
||||
</span>
|
||||
</Button>
|
||||
@@ -98,33 +89,33 @@ export function WalletAddress(props: {
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold">Wallet Address</h3>
|
||||
<h3 className="font-semibold text-lg">Wallet Address</h3>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={copyToClipboard}
|
||||
onClick={onCopy}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
{isCopied ? (
|
||||
{hasCopied ? (
|
||||
<Check className="h-4 w-4" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
{isCopied ? "Copied!" : "Copy"}
|
||||
{hasCopied ? "Copied!" : "Copy"}
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-sm font-mono bg-muted p-2 rounded text-center">
|
||||
<p className="rounded bg-muted p-2 text-center font-mono text-sm">
|
||||
{lessShortenedAddress}
|
||||
</p>
|
||||
<h3 className="text-lg font-semibold">Social Profiles</h3>
|
||||
{profiles.isLoading ? (
|
||||
<p className="text-sm text-muted-foreground">Loading profiles...</p>
|
||||
<h3 className="font-semibold text-lg">Social Profiles</h3>
|
||||
{profiles.isPending ? (
|
||||
<p className="text-muted-foreground text-sm">Loading profiles...</p>
|
||||
) : !profiles.data?.length ? (
|
||||
<p className="text-sm text-muted-foreground">No profiles found</p>
|
||||
<p className="text-muted-foreground text-sm">No profiles found</p>
|
||||
) : (
|
||||
profiles.data?.map((profile) => (
|
||||
<div
|
||||
className="flex flex-row gap-2 items-center"
|
||||
className="flex flex-row items-center gap-2"
|
||||
key={profile.type + profile.name}
|
||||
>
|
||||
{profile.avatar &&
|
||||
@@ -145,13 +136,13 @@ export function WalletAddress(props: {
|
||||
)}
|
||||
</Avatar>
|
||||
)}
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div className="flex flex-row gap-4 w-full items-center justify-between">
|
||||
<h4 className="text-md font-semibold">{profile.name}</h4>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex w-full flex-row items-center justify-between gap-4">
|
||||
<h4 className="font-semibold text-md">{profile.name}</h4>
|
||||
<Badge variant="outline">{profile.type}</Badge>
|
||||
</div>
|
||||
{profile.bio && (
|
||||
<p className="text-sm text-muted-foreground whitespace-normal line-clamp-1">
|
||||
<p className="line-clamp-1 whitespace-normal text-muted-foreground text-sm">
|
||||
{profile.bio}
|
||||
</p>
|
||||
)}
|
||||
@@ -162,7 +153,7 @@ export function WalletAddress(props: {
|
||||
<Button
|
||||
asChild
|
||||
variant="upsell"
|
||||
className="text-sm flex flex-row gap-2 items-center"
|
||||
className="flex flex-row items-center gap-2 text-sm"
|
||||
size="sm"
|
||||
>
|
||||
<a
|
||||
|
||||
@@ -10,7 +10,7 @@ export function ColorModeToggle() {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
return (
|
||||
<ClientOnly ssr={<Skeleton className="size-10 bg-accent border" />}>
|
||||
<ClientOnly ssr={<Skeleton className="size-10 border bg-accent" />}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
|
||||
@@ -20,12 +20,12 @@ export const CodeBlock = ({
|
||||
{({ className, tokens, getLineProps, getTokenProps }) => (
|
||||
<Card className={className}>
|
||||
{canCopy && (
|
||||
<CopyButton text={code} className="float-right p-2 m-2" />
|
||||
<CopyButton text={code} className="float-right m-2 p-2" />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"block font-mono text-sm whitespace-pre-wrap p-4 overflow-auto",
|
||||
"block overflow-auto whitespace-pre-wrap p-4 font-mono text-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "./button";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
@@ -12,20 +12,16 @@ export function CopyButton(props: {
|
||||
iconClassName?: string;
|
||||
variant?: "ghost" | "primary" | "secondary";
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const { hasCopied, onCopy } = useClipboard(props.text, 1000);
|
||||
return (
|
||||
<ToolTipLabel label="Copy">
|
||||
<Button
|
||||
variant={props.variant || "ghost"}
|
||||
aria-label="Copy"
|
||||
className={cn("h-auto w-auto p-1", props.className)}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(props.text);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
}}
|
||||
onClick={onCopy}
|
||||
>
|
||||
{isCopied ? (
|
||||
{hasCopied ? (
|
||||
<CheckIcon
|
||||
className={cn("size-4 text-green-500", props.iconClassName)}
|
||||
/>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import { CheckIcon, CopyIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "./button";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
@@ -22,15 +22,15 @@ export function CopyTextButton(props: {
|
||||
copyIconPosition: "left" | "right";
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
const copyButton = isCopied ? (
|
||||
const { hasCopied, onCopy } = useClipboard(props.textToCopy, 1000);
|
||||
const copyButton = hasCopied ? (
|
||||
<CheckIcon
|
||||
className={cn("size-3 text-green-500 shrink-0", props.iconClassName)}
|
||||
className={cn("size-3 shrink-0 text-green-500", props.iconClassName)}
|
||||
/>
|
||||
) : (
|
||||
<CopyIcon
|
||||
className={cn(
|
||||
"size-3 text-muted-foreground shrink-0",
|
||||
"size-3 shrink-0 text-muted-foreground",
|
||||
props.iconClassName,
|
||||
)}
|
||||
/>
|
||||
@@ -42,26 +42,24 @@ export function CopyTextButton(props: {
|
||||
variant={props.variant || "outline"}
|
||||
aria-label={props.tooltip}
|
||||
className={cn(
|
||||
"h-auto w-auto px-2.5 py-1.5 flex gap-2 rounded-lg text-foreground font-normal",
|
||||
"flex h-auto w-auto gap-2 rounded-lg px-2.5 py-1.5 font-normal text-foreground",
|
||||
props.className,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
navigator.clipboard.writeText(props.textToCopy);
|
||||
setIsCopied(true);
|
||||
setTimeout(() => setIsCopied(false), 1000);
|
||||
onCopy();
|
||||
e.stopPropagation();
|
||||
props.onClick?.();
|
||||
}}
|
||||
>
|
||||
{props.copyIconPosition === "right" ? (
|
||||
<>
|
||||
<span className="truncate min-w-0"> {props.textToShow} </span>
|
||||
<span className="min-w-0 truncate"> {props.textToShow} </span>
|
||||
{copyButton}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{copyButton}
|
||||
<span className="truncate min-w-0"> {props.textToShow} </span>
|
||||
<span className="min-w-0 truncate"> {props.textToShow} </span>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
@@ -48,9 +48,9 @@ export function DatePickerWithRange(props: {
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="date"
|
||||
variant={"outline"}
|
||||
variant="outline"
|
||||
className={cn(
|
||||
"justify-start text-left font-normal gap-2",
|
||||
"justify-start gap-2 text-left font-normal",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -60,7 +60,7 @@ export function DatePickerWithRange(props: {
|
||||
{format(from, "LLL dd, y")} - {format(to, "LLL dd, y")}
|
||||
</>
|
||||
)}
|
||||
<ChevronDownIcon className="h-4 w-4 ml-auto text-muted-foreground" />
|
||||
<ChevronDownIcon className="ml-auto h-4 w-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
@@ -69,7 +69,7 @@ export function DatePickerWithRange(props: {
|
||||
<DynamicHeight>
|
||||
<div>
|
||||
{!isValid && (
|
||||
<p className="text-sm pt-2 justify-center text-destructive-text text-center flex items-center gap-2">
|
||||
<p className="flex items-center justify-center gap-2 pt-2 text-center text-destructive-text text-sm">
|
||||
<CalendarX2Icon className="h-4 w-4" />
|
||||
Invalid date range
|
||||
</p>
|
||||
|
||||
@@ -71,8 +71,15 @@ export function ScrollShadow(props: {
|
||||
});
|
||||
});
|
||||
content.addEventListener("scroll", handleScroll);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleScroll();
|
||||
});
|
||||
|
||||
resizeObserver.observe(content);
|
||||
return () => {
|
||||
content.removeEventListener("scroll", handleScroll);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -87,7 +94,7 @@ export function ScrollShadow(props: {
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`${styles.scrollShadowTop} ${styles.scrollShadowY}`}
|
||||
className={cn(styles.scrollShadowTop, styles.scrollShadowY)}
|
||||
ref={shadowTopEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
@@ -95,28 +102,28 @@ export function ScrollShadow(props: {
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.scrollShadowBottom} ${styles.scrollShadowY}`}
|
||||
className={cn(styles.scrollShadowBottom, styles.scrollShadowY)}
|
||||
ref={shadowBottomEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.scrollShadowLeft} ${styles.scrollShadowX}`}
|
||||
className={cn(styles.scrollShadowLeft, styles.scrollShadowX)}
|
||||
ref={shadowLeftEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={`${styles.scrollShadowRight} ${styles.scrollShadowX}`}
|
||||
className={cn(styles.scrollShadowRight, styles.scrollShadowX)}
|
||||
ref={shadowRightEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn("overflow-auto no-scrollbar", props.scrollableClassName)}
|
||||
className={cn("no-scrollbar overflow-auto", props.scrollableClassName)}
|
||||
ref={scrollableEl}
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { cn } from "../../../lib/utils";
|
||||
import style from "./Spinner.module.css";
|
||||
|
||||
export function Spinner(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 50 50"
|
||||
className={`${style.loader} ${props.className || "size-4"}`}
|
||||
className={cn(style.loader, props.className || "size-4")}
|
||||
>
|
||||
<title>loading</title>
|
||||
<circle
|
||||
|
||||
@@ -19,7 +19,7 @@ const AlertDialogOverlay = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -37,7 +37,7 @@ const AlertDialogContent = React.forwardRef<
|
||||
<AlertDialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] rounded-lg",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] fixed top-[50%] left-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -80,7 +80,7 @@ const AlertDialogTitle = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn("text-lg hyphens-auto font-semibold", className)}
|
||||
className={cn("hyphens-auto font-semibold text-lg", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -92,7 +92,7 @@ const AlertDialogDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AlertDialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -52,7 +52,7 @@ const AlertDescription = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-sm [&_p]:leading-relaxed text-muted-foreground mt-1.5",
|
||||
"mt-1.5 text-muted-foreground text-sm [&_p]:leading-relaxed",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio";
|
||||
|
||||
const AspectRatio = AspectRatioPrimitive.Root;
|
||||
|
||||
export { AspectRatio };
|
||||
@@ -19,7 +19,7 @@ const BreadcrumbList = React.forwardRef<
|
||||
<ol
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-sm text-muted-foreground sm:gap-2.5",
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-muted-foreground text-sm sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -20,7 +20,7 @@ export const AllVariants: Story = {
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6 text-foregroun flex flex-col gap-6">
|
||||
<div className="flex min-h-screen flex-col gap-6 bg-background p-6 text-foregroun">
|
||||
<Variants size="default" />
|
||||
<Variants size="sm" />
|
||||
<Variants size="icon" />
|
||||
|
||||
@@ -32,7 +32,7 @@ const CardTitle = React.forwardRef<
|
||||
<h3
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-2xl font-semibold leading-none tracking-tight",
|
||||
"font-semibold text-2xl leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -46,7 +46,7 @@ const CardDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<p
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -233,7 +233,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 gap-2 justify-between leading-none",
|
||||
"flex flex-1 justify-between gap-2 leading-none",
|
||||
nestLabel ? "items-end" : "items-center",
|
||||
)}
|
||||
>
|
||||
@@ -244,7 +244,7 @@ const ChartTooltipContent = React.forwardRef<
|
||||
</span>
|
||||
</div>
|
||||
{item.value && (
|
||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
||||
<span className="font-medium font-mono text-foreground tabular-nums">
|
||||
{valueFormatter
|
||||
? valueFormatter(item.value) || item.value
|
||||
: item.value}
|
||||
@@ -287,7 +287,7 @@ const ChartLegendContent = React.forwardRef<
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex items-center justify-center gap-x-4 gap-y-2 overflow-x-auto flex-wrap",
|
||||
"flex flex-wrap items-center justify-center gap-x-4 gap-y-2 overflow-x-auto",
|
||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -37,7 +37,7 @@ export function CheckboxWithLabel(props: {
|
||||
// biome-ignore lint/a11y/noLabelWithoutControl: cannot do this here because the checkbox is a child of the label
|
||||
<label
|
||||
className={cn(
|
||||
"flex gap-2 items-start text-muted-foreground text-sm cursor-pointer select-none [&>button]:mt-0.5",
|
||||
"flex cursor-pointer select-none items-start gap-2 text-muted-foreground text-sm [&>button]:mt-0.5",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef<
|
||||
<DialogPrimitive.Overlay
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed inset-0 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -41,16 +41,16 @@ const DialogContent = React.forwardRef<
|
||||
<DialogPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"fixed grid w-full md:max-w-lg gap-4 border bg-background p-6 shadow-lg duration-300 rounded-t-xl md:rounded-lg border-border",
|
||||
"fixed grid w-full gap-4 rounded-t-xl border border-border bg-background p-6 shadow-lg duration-300 md:max-w-lg md:rounded-lg",
|
||||
// on mobile - put the dialog at the bottom of the screen, animate - slide up and fade in
|
||||
"bottom-0 left-0 right-0",
|
||||
"right-0 bottom-0 left-0",
|
||||
"data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
|
||||
// on desktop - vertically and horizontally center the dialog, animate - zoom and fade in
|
||||
"md:bottom-auto md:left-[50%] md:top-[50%] md:translate-x-[-50%] md:translate-y-[-50%] ",
|
||||
"md:top-[50%] md:bottom-auto md:left-[50%] md:translate-x-[-50%] md:translate-y-[-50%] ",
|
||||
"md:data-[state=closed]:zoom-out-95 md:data-[state=open]:zoom-in-95 ",
|
||||
"md:data-[state=closed]:slide-out-to-left-1/2 md:data-[state=closed]:slide-out-to-top-[48%] ",
|
||||
"md:data-[state=open]:slide-in-from-left-1/2 md:data-[state=open]:slide-in-from-top-[48%] ",
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out",
|
||||
"data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
className,
|
||||
)}
|
||||
@@ -59,7 +59,7 @@ const DialogContent = React.forwardRef<
|
||||
{children}
|
||||
<DialogPrimitive.Close
|
||||
className={cn(
|
||||
"absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
||||
"absolute top-4 right-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground",
|
||||
props.dialogCloseClassName,
|
||||
)}
|
||||
>
|
||||
@@ -103,7 +103,7 @@ const DialogTitle = React.forwardRef<
|
||||
<DialogPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold leading-none tracking-tight",
|
||||
"font-semibold text-lg leading-none tracking-tight",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -117,7 +117,7 @@ const DialogDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<DialogPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
|
||||
<DropdownMenuPrimitive.SubContent
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -99,7 +99,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.CheckboxItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
checked={checked}
|
||||
@@ -123,7 +123,7 @@ const DropdownMenuRadioItem = React.forwardRef<
|
||||
<DropdownMenuPrimitive.RadioItem
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -147,7 +147,7 @@ const DropdownMenuLabel = React.forwardRef<
|
||||
<DropdownMenuPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-sm font-semibold",
|
||||
"px-2 py-1.5 font-semibold text-sm",
|
||||
inset && "pl-8",
|
||||
className,
|
||||
)}
|
||||
|
||||
@@ -117,7 +117,7 @@ const RequiredFormLabel = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{props.children}
|
||||
<span className="absolute -top-1.5 -right-2 text-destructive-text">
|
||||
<span className="-top-1.5 -right-2 absolute text-destructive-text">
|
||||
•
|
||||
</span>
|
||||
</Label>
|
||||
@@ -159,7 +159,7 @@ const FormDescription = React.forwardRef<
|
||||
<p
|
||||
ref={ref}
|
||||
id={formDescriptionId}
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -181,7 +181,7 @@ const FormMessage = React.forwardRef<
|
||||
<p
|
||||
ref={ref}
|
||||
id={formMessageId}
|
||||
className={cn("text-sm font-medium text-destructive-text", className)}
|
||||
className={cn("font-medium text-destructive-text text-sm", className)}
|
||||
{...props}
|
||||
>
|
||||
{body}
|
||||
|
||||
@@ -18,7 +18,7 @@ const HoverCardContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -31,7 +31,7 @@ const ImageUpload = React.forwardRef<HTMLInputElement, ImageUploadProps>(
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
"border bg-card relative group cursor-pointer hover:border-primary flex-col gap-1.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 overflow-hidden transition-all rounded-md flex items-center justify-center p-4 py-6 text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"group relative flex cursor-pointer flex-col items-center justify-center gap-1.5 overflow-hidden rounded-md border bg-card p-4 py-6 text-muted-foreground transition-all hover:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
isDragActive ? "border-primary" : "border-input",
|
||||
!activeFile ? "w-full" : "min-h-32 min-w-32",
|
||||
className,
|
||||
@@ -47,8 +47,8 @@ const ImageUpload = React.forwardRef<HTMLInputElement, ImageUploadProps>(
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<UploadIcon className="w-5 h-5 transition-all group-hover:scale-110" />
|
||||
<p className="text-xs text-center">
|
||||
<UploadIcon className="h-5 w-5 transition-all group-hover:scale-110" />
|
||||
<p className="text-center text-xs">
|
||||
Drag and drop a file or click to upload
|
||||
</p>
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function InlineCode({
|
||||
code,
|
||||
className,
|
||||
}: { code: string; className?: string }) {
|
||||
return (
|
||||
<code
|
||||
className={cn(
|
||||
"inline-block rounded bg-muted px-2 font-mono text-sm",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{code}
|
||||
</code>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import { OTPInput, OTPInputContext } from "input-otp";
|
||||
import { Dot } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const InputOTP = React.forwardRef<
|
||||
React.ElementRef<typeof OTPInput>,
|
||||
React.ComponentPropsWithoutRef<typeof OTPInput>
|
||||
>(({ className, containerClassName, ...props }, ref) => (
|
||||
<OTPInput
|
||||
ref={ref}
|
||||
containerClassName={cn(
|
||||
"flex items-center gap-2 has-[:disabled]:opacity-50",
|
||||
containerClassName,
|
||||
)}
|
||||
className={cn("disabled:cursor-not-allowed", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
InputOTP.displayName = "InputOTP";
|
||||
|
||||
const InputOTPGroup = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("flex items-center", className)} {...props} />
|
||||
));
|
||||
InputOTPGroup.displayName = "InputOTPGroup";
|
||||
|
||||
const InputOTPSlot = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div"> & { index: number }
|
||||
>(({ index, className, ...props }, ref) => {
|
||||
const inputOTPContext = React.useContext(OTPInputContext);
|
||||
const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index];
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex h-10 w-10 items-center justify-center border-input border-y border-r text-sm transition-all first:rounded-l-md first:border-l last:rounded-r-md",
|
||||
isActive && "z-10 ring-2 ring-ring ring-offset-background",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{char}
|
||||
{hasFakeCaret && (
|
||||
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
||||
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
InputOTPSlot.displayName = "InputOTPSlot";
|
||||
|
||||
const InputOTPSeparator = React.forwardRef<
|
||||
React.ElementRef<"div">,
|
||||
React.ComponentPropsWithoutRef<"div">
|
||||
>(({ ...props }, ref) => (
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: <explanation>
|
||||
<div ref={ref} role="separator" {...props}>
|
||||
<Dot />
|
||||
</div>
|
||||
));
|
||||
InputOTPSeparator.displayName = "InputOTPSeparator";
|
||||
|
||||
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
||||
@@ -11,7 +11,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"flex h-10 w-full rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
|
||||
@@ -46,7 +46,7 @@ const PaginationLink = ({
|
||||
<Button
|
||||
variant={!isActive ? "outline" : "default"}
|
||||
className={cn(
|
||||
"px-3 py-2 h-auto",
|
||||
"h-auto px-3 py-2",
|
||||
className,
|
||||
props.disabled && "cursor-not-allowed",
|
||||
)}
|
||||
|
||||
@@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 rounded-md border border-border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -29,7 +29,7 @@ const RadioGroupItem = React.forwardRef<
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"aspect-square h-4 w-4 rounded-full border border-primary text-primary ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"aspect-square h-4 w-4 rounded-full border border-inverted text-inverted ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -51,19 +51,19 @@ const RadioGroupItemButton = React.forwardRef<
|
||||
<RadioGroupPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"cursor-pointer group flex items-center px-3 peer-hover:border-nonce py-4 space-x-3 space-y-0 border-2 rounded-md min-w-32 font-medium hover:border-foreground/25 transition-all data-[state=checked]:border-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
"group flex min-w-32 cursor-pointer items-center space-x-3 space-y-0 rounded-md border-2 px-3 py-4 font-medium transition-all hover:border-foreground/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 peer-hover:border-nonce data-[state=checked]:border-inverted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-center justify-center w-4 h-4 border-2 rounded-full transition-all group-data-[state=checked]:border-primary text-primary group-hover:border-foreground/25 aspect-square ring-offset-background focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50">
|
||||
<div className="flex aspect-square h-4 w-4 items-center justify-center rounded-full border-2 text-inverted ring-offset-background transition-all focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 group-hover:border-foreground/25 group-data-[state=checked]:border-inverted">
|
||||
{/* Show on checked */}
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center transition-all rounded-full">
|
||||
<Circle className="w-2 h-2 text-current fill-current" />
|
||||
<RadioGroupPrimitive.Indicator className="flex items-center justify-center rounded-full transition-all">
|
||||
<Circle className="h-2 w-2 fill-current text-current" />
|
||||
</RadioGroupPrimitive.Indicator>
|
||||
{/* Show on hover */}
|
||||
<div className="items-center justify-center hidden transition-all rounded-full text-foreground/25 group-hover:flex group-data-[state=checked]:hidden">
|
||||
<Circle className="w-2 h-2 text-current fill-current" />
|
||||
<div className="hidden items-center justify-center rounded-full text-foreground/25 transition-all group-hover:flex group-data-[state=checked]:hidden">
|
||||
<Circle className="h-2 w-2 fill-current text-current" />
|
||||
</div>
|
||||
</div>
|
||||
<Label className="cursor-pointer">{props.children}</Label>
|
||||
|
||||
@@ -31,7 +31,7 @@ function randomName() {
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background p-6 text-foregroun flex flex-col gap-10">
|
||||
<div className="flex min-h-screen flex-col gap-10 bg-background p-6 text-foregroun">
|
||||
<BadgeContainer label="5 items, no placeholder, no label, value filled">
|
||||
<SelectDemo listItems={5} selectFirst />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -19,7 +19,7 @@ const SelectTrigger = React.forwardRef<
|
||||
<SelectPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex gap-2 h-10 w-full items-center justify-between rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
"flex h-10 w-full items-center justify-between gap-2 rounded-md border border-input bg-card px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -87,9 +87,9 @@ const SelectContent = React.forwardRef<
|
||||
};
|
||||
}}
|
||||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border border-border bg-popover text-popover-foreground shadow-md data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
"data-[side=left]:-translate-x-1 data-[side=top]:-translate-y-1 data-[side=right]:translate-x-1 data-[side=bottom]:translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
@@ -117,7 +117,7 @@ const SelectLabel = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SelectPrimitive.Label
|
||||
ref={ref}
|
||||
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||
className={cn("py-1.5 pr-2 pl-8 font-semibold text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -130,7 +130,7 @@ const SelectItem = React.forwardRef<
|
||||
<SelectPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -21,7 +21,7 @@ const SheetOverlay = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Overlay
|
||||
className={cn(
|
||||
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
|
||||
"data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80 data-[state=closed]:animate-out data-[state=open]:animate-in",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -65,7 +65,7 @@ const SheetContent = React.forwardRef<
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary border-border">
|
||||
<SheetPrimitive.Close className="absolute top-4 right-4 rounded-sm border-border opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</SheetPrimitive.Close>
|
||||
@@ -80,7 +80,7 @@ const SheetHeader = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col space-y-2 text-center sm:text-left border-border",
|
||||
"flex flex-col space-y-2 border-border text-center sm:text-left",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -94,7 +94,7 @@ const SheetFooter = ({
|
||||
}: React.HTMLAttributes<HTMLDivElement>) => (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2 border-border",
|
||||
"flex flex-col-reverse border-border sm:flex-row sm:justify-end sm:space-x-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -109,7 +109,7 @@ const SheetTitle = React.forwardRef<
|
||||
<SheetPrimitive.Title
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"text-lg font-semibold text-foreground border-border",
|
||||
"border-border font-semibold text-foreground text-lg",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -123,7 +123,7 @@ const SheetDescription = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SheetPrimitive.Description
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground border-border", className)}
|
||||
className={cn("border-border text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
@@ -25,7 +25,7 @@ function SkeletonContainer<T>(props: {
|
||||
style={props.style}
|
||||
aria-hidden={isLoading ? "true" : "false"}
|
||||
className={cn(
|
||||
isLoading && "animate-pulse rounded-lg bg-muted inline-block",
|
||||
isLoading && "inline-block animate-pulse rounded-lg bg-muted",
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -36,7 +36,7 @@ export const Mobile: Story = {
|
||||
|
||||
function Component() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6 lg:max-w-[1000px] lg:mx-auto min-w-0 py-6 px-4">
|
||||
<div className="flex min-w-0 flex-col gap-6 px-4 py-6 lg:mx-auto lg:max-w-[1000px]">
|
||||
<BadgeContainer label="Normal">
|
||||
<TableDemo />
|
||||
</BadgeContainer>
|
||||
|
||||
@@ -10,7 +10,7 @@ const Table = React.forwardRef<
|
||||
<table
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"caption-bottom w-full text-sm border-collapse tabular-nums lining-nums align-top",
|
||||
"w-full caption-bottom border-collapse align-top text-sm lining-nums tabular-nums",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -25,7 +25,7 @@ const TableHeader = React.forwardRef<
|
||||
<thead
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"bg-muted/50 border-b border-border [&_tr]:border-b",
|
||||
"border-border border-b bg-muted/50 [&_tr]:border-b",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -53,7 +53,7 @@ const TableFooter = React.forwardRef<
|
||||
<tfoot
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-t border-border bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
"border-border border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -68,7 +68,7 @@ const TableRow = React.forwardRef<
|
||||
<tr
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"border-b border-border last:border-0 data-[state=selected]:bg-muted",
|
||||
"border-border border-b last:border-0 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -83,7 +83,7 @@ const TableHead = React.forwardRef<
|
||||
<th
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-6 py-4 uppercase text-xs text-left tracking-wider align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0",
|
||||
"px-6 py-4 text-left align-middle font-medium text-muted-foreground text-xs uppercase tracking-wider [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -98,7 +98,7 @@ const TableCell = React.forwardRef<
|
||||
<td
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"px-6 py-4 text-sm text-start align-middle [&:has([role=checkbox])]:pr-0",
|
||||
"px-6 py-4 text-start align-middle text-sm [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -112,7 +112,7 @@ const TableCaption = React.forwardRef<
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
ref={ref}
|
||||
className={cn("text-sm text-muted-foreground py-4", className)}
|
||||
className={cn("py-4 text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
@@ -121,7 +121,7 @@ TableCaption.displayName = "TableCaption";
|
||||
function TableContainer(props: { children: React.ReactNode }) {
|
||||
return (
|
||||
<ScrollShadow
|
||||
className="border border-border rounded-lg whitespace-nowrap relative"
|
||||
className="relative whitespace-nowrap rounded-lg border border-border"
|
||||
shadowColor="hsl(var(--muted))"
|
||||
>
|
||||
{props.children}
|
||||
|
||||
@@ -27,7 +27,7 @@ export function TabLinks(props: {
|
||||
return (
|
||||
<div className={cn("relative", props.className)}>
|
||||
{/* Bottom line */}
|
||||
<div className="h-[1px] bg-border absolute bottom-0 left-0 right-0" />
|
||||
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
|
||||
|
||||
<ScrollShadow
|
||||
scrollableClassName="pb-[8px] relative"
|
||||
@@ -51,7 +51,7 @@ export function TabLinks(props: {
|
||||
href={tab.href}
|
||||
aria-disabled={tab.isDisabled}
|
||||
className={cn(
|
||||
"rounded-lg hover:bg-muted px-3 font-normal text-sm lg:text-sm relative h-auto text-muted-foreground",
|
||||
"relative h-auto rounded-lg px-3 font-normal text-muted-foreground text-sm hover:bg-muted lg:text-sm",
|
||||
!tab.isActive && !tab.isDisabled && "hover:text-foreground",
|
||||
tab.isDisabled && "pointer-events-none",
|
||||
tab.isActive && "!text-foreground",
|
||||
@@ -67,7 +67,7 @@ export function TabLinks(props: {
|
||||
{/* Active line */}
|
||||
<div
|
||||
ref={lineRef}
|
||||
className="absolute left-0 bottom-0 h-[2px] bg-foreground rounded-lg fade-in-0 animate-in"
|
||||
className="fade-in-0 absolute bottom-0 left-0 h-[2px] animate-in rounded-lg bg-foreground"
|
||||
/>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
@@ -94,7 +94,7 @@ export function TabButtons(props: {
|
||||
return (
|
||||
<div className={cn("relative", props.containerClassName)}>
|
||||
{/* Bottom line */}
|
||||
<div className="h-[1px] bg-border absolute bottom-0 left-0 right-0" />
|
||||
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
|
||||
|
||||
<ScrollShadow
|
||||
scrollableClassName="pb-[8px] relative"
|
||||
@@ -111,7 +111,7 @@ export function TabButtons(props: {
|
||||
variant="ghost"
|
||||
ref={tab.isActive ? activeTabRef : undefined}
|
||||
className={cn(
|
||||
"rounded-lg hover:bg-accent px-2 lg:px-3 font-medium text-sm lg:text-base relative h-auto inline-flex gap-1.5 items-center",
|
||||
"relative inline-flex h-auto items-center gap-1.5 rounded-lg px-2 font-medium text-sm hover:bg-accent lg:px-3 lg:text-base",
|
||||
!tab.isActive &&
|
||||
"text-muted-foreground hover:text-foreground",
|
||||
!tab.isEnabled && "cursor-not-allowed opacity-50",
|
||||
@@ -130,7 +130,7 @@ export function TabButtons(props: {
|
||||
{/* Active line */}
|
||||
<div
|
||||
ref={lineRef}
|
||||
className="absolute left-0 bottom-0 h-[2px] bg-foreground rounded-lg fade-in-0 animate-in"
|
||||
className="fade-in-0 absolute bottom-0 left-0 h-[2px] animate-in rounded-lg bg-foreground"
|
||||
/>
|
||||
</ScrollShadow>
|
||||
</div>
|
||||
@@ -165,6 +165,8 @@ function useUnderline<El extends HTMLElement>() {
|
||||
block: "nearest",
|
||||
inline: "center",
|
||||
});
|
||||
} else if (lineRef.current) {
|
||||
lineRef.current.style.width = "0px";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ const TooltipContent = React.forwardRef<
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md border border-border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"fade-in-0 zoom-in-95 data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 animate-in overflow-hidden rounded-md border border-border bg-popover px-3 py-1.5 text-popover-foreground text-sm shadow-md data-[state=closed]:animate-out",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -48,9 +48,9 @@ export function ToolTipLabel(props: {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
sideOffset={10}
|
||||
className="max-w-[400px] leading-relaxed whitespace-normal"
|
||||
className="max-w-[400px] whitespace-normal leading-relaxed"
|
||||
>
|
||||
<div className="p-2 text-sm flex items-center gap-1.5">
|
||||
<div className="flex items-center gap-1.5 p-2 text-sm">
|
||||
{props.leftIcon}
|
||||
{props.label}
|
||||
{props.rightIcon}
|
||||
|
||||
@@ -20,3 +20,6 @@ export const PROD_OR_DEV_URL = isProd ? "thirdweb.com" : "thirdweb-dev.com";
|
||||
export const DASHBOARD_STORAGE_URL =
|
||||
process.env.NEXT_PUBLIC_DASHBOARD_UPLOAD_SERVER ||
|
||||
"https://storage.thirdweb.com";
|
||||
|
||||
export const API_SERVER_URL =
|
||||
process.env.NEXT_PUBLIC_THIRDWEB_API_HOST || "https://api.thirdweb.com";
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {} from "@/constants/env";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useMemo } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
|
||||
@@ -117,7 +117,7 @@ function DashboardRouterTopProgressBarInner() {
|
||||
const width = isLoading ? progress : 100;
|
||||
return (
|
||||
<span
|
||||
className="block fixed top-0 h-[2px] bg-blue-500"
|
||||
className="fixed top-0 block h-[2px] bg-blue-500"
|
||||
style={{
|
||||
opacity: isLoading ? "100" : "0",
|
||||
width: `${width}%`,
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
type Store<T> = {
|
||||
"use client";
|
||||
|
||||
import { useSyncExternalStore } from "react";
|
||||
|
||||
export type Store<T> = {
|
||||
getValue(): T;
|
||||
setValue(newValue: T): void;
|
||||
subscribe(listener: () => void): () => void;
|
||||
@@ -45,3 +49,7 @@ export function createStore<T>(initialValue: T): Store<T> {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useStore<T>(store: Store<T>): T {
|
||||
return useSyncExternalStore(store.subscribe, store.getValue, store.getValue);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
/**
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
export function useShowMore<T extends HTMLElement>(
|
||||
initialItemsToShow: number,
|
||||
itemsToAdd: number,
|
||||
) {
|
||||
// start with showing first `initialItemsToShow` items, when the last item is in view, show `itemsToAdd` more
|
||||
const [itemsToShow, setItemsToShow] = useState(initialItemsToShow);
|
||||
const lastItemRef = useCallback(
|
||||
(node: T) => {
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0]?.isIntersecting) {
|
||||
setItemsToShow((prev) => prev + itemsToAdd); // show 10 more items
|
||||
}
|
||||
},
|
||||
{ threshold: 1 },
|
||||
);
|
||||
|
||||
observer.observe(node);
|
||||
// when the node is removed from the DOM, observer will be disconnected automatically by the browser
|
||||
},
|
||||
[itemsToAdd],
|
||||
);
|
||||
|
||||
return { itemsToShow, lastItemRef };
|
||||
}
|
||||
@@ -140,7 +140,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
/* Fix colors on autofilled inputs */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
input:-webkit-autofill:focus,
|
||||
input:-webkit-autofill:active {
|
||||
/* Revert text color */
|
||||
-webkit-text-fill-color: hsl(var(--foreground)) !important;
|
||||
color: hsl(var(--foreground)) !important;
|
||||
caret-color: hsl(var(--foreground)) !important;
|
||||
|
||||
/* Revert background color */
|
||||
transition: background-color 5000s ease-in-out 0s;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# DO NOT ADD NEW THINGS INSIDE THIS FOLDER OR SUB_FOLDERS
|
||||
|
||||
This part of the dashboard is deprecated, and we are actively working on removing it. Please do not add new things here. If you need to add new things, please add them to the appropriate folder in the src folder.
|
||||
@@ -1,9 +1,3 @@
|
||||
export const networkKeys = {
|
||||
all: ["network"] as const,
|
||||
chain: (chainId?: number) => [...networkKeys.all, chainId] as const,
|
||||
multiChainRegistry: ["multi-chain-registry"] as const,
|
||||
};
|
||||
|
||||
export const accountKeys = {
|
||||
all: ["account"] as const,
|
||||
wallet: (walletAddress: string) =>
|
||||
@@ -104,4 +98,11 @@ export const engineKeys = {
|
||||
[...engineKeys.all, engineId, "systemMetrics"] as const,
|
||||
queueMetrics: (engineId: string) =>
|
||||
[...engineKeys.all, engineId, "queueMetrics"] as const,
|
||||
|
||||
alertRules: (engineId: string) =>
|
||||
[...engineKeys.all, engineId, "alertRules"] as const,
|
||||
alerts: (engineId: string) =>
|
||||
[...engineKeys.all, engineId, "alerts"] as const,
|
||||
notificationChannels: (engineId: string) =>
|
||||
[...engineKeys.all, engineId, "notificationChannels"] as const,
|
||||
};
|
||||
|
||||
@@ -3,27 +3,31 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useThirdwebClient } from "@/constants/thirdweb.client";
|
||||
import { useStore } from "@/lib/reactive";
|
||||
import { getSDKTheme } from "app/components/sdk-component-theme";
|
||||
import { CustomChainRenderer } from "components/selects/CustomChainRenderer";
|
||||
import { mapV4ChainToV5Chain } from "contexts/map-chains";
|
||||
import { useTrack } from "hooks/analytics/useTrack";
|
||||
import {
|
||||
useSupportedChains,
|
||||
useSupportedChainsRecord,
|
||||
} from "hooks/chains/configureChains";
|
||||
import {
|
||||
useAddRecentlyUsedChainId,
|
||||
useRecentlyUsedChains,
|
||||
} from "hooks/chains/recentlyUsedChains";
|
||||
import { useSetIsNetworkConfigModalOpen } from "hooks/networkConfigModal";
|
||||
import { useTheme } from "next-themes";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import type { Chain } from "thirdweb";
|
||||
import { AutoConnect, ConnectButton, useConnectModal } from "thirdweb/react";
|
||||
import { useFavoriteChains } from "../../hooks/useFavoriteChains";
|
||||
import {
|
||||
AutoConnect,
|
||||
ConnectButton,
|
||||
type NetworkSelectorProps,
|
||||
useConnectModal,
|
||||
} from "thirdweb/react";
|
||||
import { useFavoriteChainIds } from "../../../../app/(dashboard)/(chain)/components/client/star-button";
|
||||
import { LazyConfigureNetworkModal } from "../../../../components/configure-networks/LazyConfigureNetworkModal";
|
||||
import { useAllChainsData } from "../../../../hooks/chains/allChains";
|
||||
import {
|
||||
type StoredChain,
|
||||
addRecentlyUsedChainId,
|
||||
recentlyUsedChainIdsStore,
|
||||
} from "../../../../stores/chainStores";
|
||||
import { useLoggedInUser } from "../../hooks/useLoggedInUser";
|
||||
import { popularChains } from "../popularChains";
|
||||
|
||||
@@ -35,51 +39,93 @@ export const CustomConnectWallet = (props: {
|
||||
const thirdwebClient = useThirdwebClient();
|
||||
const loginRequired =
|
||||
props.loginRequired === undefined ? true : props.loginRequired;
|
||||
|
||||
const { theme } = useTheme();
|
||||
const recentChainsv4 = useRecentlyUsedChains();
|
||||
const addRecentlyUsedChainId = useAddRecentlyUsedChainId();
|
||||
// const setIsNetworkConfigModalOpen = useSetIsNetworkConfigModalOpen();
|
||||
const t = theme === "light" ? "light" : "dark";
|
||||
const allv4Chains = useSupportedChains();
|
||||
const chainsRecord = useSupportedChainsRecord();
|
||||
const favChainsQuery = useFavoriteChains();
|
||||
const setIsNetworkConfigModalOpen = useSetIsNetworkConfigModalOpen();
|
||||
const allChains = useMemo(() => {
|
||||
return allv4Chains.map(mapV4ChainToV5Chain);
|
||||
}, [allv4Chains]);
|
||||
|
||||
const popularChainsWithMeta = useMemo(() => {
|
||||
return popularChains.map((x) => chainsRecord[x.id] || x);
|
||||
}, [chainsRecord]);
|
||||
// chains
|
||||
const favChainIdsQuery = useFavoriteChainIds();
|
||||
const recentChainIds = useStore(recentlyUsedChainIdsStore);
|
||||
const { idToChain, allChains } = useAllChainsData();
|
||||
|
||||
const chainSections = useMemo(() => {
|
||||
const allChainsWithMetadata = useMemo(
|
||||
() => allChains.map(mapV4ChainToV5Chain),
|
||||
[allChains],
|
||||
);
|
||||
|
||||
const recentlyUsedChainsWithMetadata = useMemo(
|
||||
() =>
|
||||
recentChainIds
|
||||
.map((id) => {
|
||||
const c = idToChain.get(id);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return c ? mapV4ChainToV5Chain(c) : undefined;
|
||||
})
|
||||
.filter((x) => !!x),
|
||||
[recentChainIds, idToChain],
|
||||
);
|
||||
|
||||
const favoriteChainsWithMetadata = useMemo(() => {
|
||||
return (favChainIdsQuery.data || [])
|
||||
.map((id) => {
|
||||
const c = idToChain.get(Number(id));
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return c ? mapV4ChainToV5Chain(c) : undefined;
|
||||
})
|
||||
.filter((x) => !!x);
|
||||
}, [idToChain, favChainIdsQuery.data]);
|
||||
|
||||
const popularChainsWithMetadata = useMemo(() => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return popularChains.map((x) =>
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
{
|
||||
const v4Chain = idToChain.get(x.id);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return v4Chain ? mapV4ChainToV5Chain(v4Chain) : x;
|
||||
},
|
||||
);
|
||||
}, [idToChain]);
|
||||
|
||||
// Network Config Modal
|
||||
const [isNetworkConfigModalOpen, setIsNetworkConfigModalOpen] =
|
||||
useState(false);
|
||||
const [editChain, setEditChain] = useState<StoredChain | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const chainSections: NetworkSelectorProps["sections"] = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
label: "Favorites",
|
||||
chains: favChainsQuery.data.map(mapV4ChainToV5Chain),
|
||||
},
|
||||
{
|
||||
label: "Popular",
|
||||
chains: popularChainsWithMeta.map(mapV4ChainToV5Chain),
|
||||
chains: favoriteChainsWithMetadata,
|
||||
},
|
||||
{
|
||||
label: "Recent",
|
||||
chains: recentChainsv4.map(mapV4ChainToV5Chain),
|
||||
chains: recentlyUsedChainsWithMetadata,
|
||||
},
|
||||
{
|
||||
label: "Popular",
|
||||
chains: popularChainsWithMetadata,
|
||||
},
|
||||
];
|
||||
}, [recentChainsv4, favChainsQuery.data, popularChainsWithMeta]);
|
||||
}, [
|
||||
recentlyUsedChainsWithMetadata,
|
||||
favoriteChainsWithMetadata,
|
||||
popularChainsWithMetadata,
|
||||
]);
|
||||
|
||||
// ensures login status on pages that need it
|
||||
const { isLoading, isLoggedIn } = useLoggedInUser();
|
||||
const { isPending, isLoggedIn } = useLoggedInUser();
|
||||
const pathname = usePathname();
|
||||
|
||||
if (isLoading) {
|
||||
if (isPending) {
|
||||
return (
|
||||
<>
|
||||
<div className="w-[144px] h-[48px] bg-muted border border-border rounded-lg flex items-center justify-center">
|
||||
<div className="flex h-[48px] w-[144px] items-center justify-center rounded-lg border border-border bg-muted">
|
||||
<Spinner className="size-4" />
|
||||
</div>
|
||||
{/* need autoconnect here so that we actually connect */}
|
||||
{/* need auto connect here so that we actually connect */}
|
||||
<AutoConnect client={thirdwebClient} />
|
||||
</>
|
||||
);
|
||||
@@ -95,57 +141,76 @@ export const CustomConnectWallet = (props: {
|
||||
Sign In
|
||||
</Link>
|
||||
</Button>
|
||||
{/* need autoconnect here so that we actually connect */}
|
||||
{/* need auto connect here so that we actually connect */}
|
||||
<AutoConnect client={thirdwebClient} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConnectButton
|
||||
theme={getSDKTheme(t)}
|
||||
client={thirdwebClient}
|
||||
connectModal={{
|
||||
privacyPolicyUrl: "/privacy",
|
||||
termsOfServiceUrl: "/tos",
|
||||
showThirdwebBranding: false,
|
||||
welcomeScreen: () => <ConnectWalletWelcomeScreen theme={t} />,
|
||||
}}
|
||||
appMetadata={{
|
||||
name: "thirdweb",
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
onDisconnect={async () => {
|
||||
try {
|
||||
// log out the user
|
||||
await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to log out", err);
|
||||
}
|
||||
}}
|
||||
connectButton={{
|
||||
className: props.connectButtonClassName,
|
||||
}}
|
||||
detailsButton={{
|
||||
className: props.detailsButtonClassName,
|
||||
}}
|
||||
chains={allChains}
|
||||
detailsModal={{
|
||||
networkSelector: {
|
||||
sections: chainSections,
|
||||
onSwitch(chain) {
|
||||
addRecentlyUsedChainId(chain.id);
|
||||
<>
|
||||
<ConnectButton
|
||||
theme={getSDKTheme(t)}
|
||||
client={thirdwebClient}
|
||||
connectModal={{
|
||||
privacyPolicyUrl: "/privacy",
|
||||
termsOfServiceUrl: "/tos",
|
||||
showThirdwebBranding: false,
|
||||
welcomeScreen: () => <ConnectWalletWelcomeScreen theme={t} />,
|
||||
}}
|
||||
appMetadata={{
|
||||
name: "thirdweb",
|
||||
logoUrl: "https://thirdweb.com/favicon.ico",
|
||||
url: "https://thirdweb.com",
|
||||
}}
|
||||
onDisconnect={async () => {
|
||||
try {
|
||||
// log out the user
|
||||
await fetch("/api/auth/logout", {
|
||||
method: "POST",
|
||||
});
|
||||
} catch (err) {
|
||||
console.error("Failed to log out", err);
|
||||
}
|
||||
}}
|
||||
connectButton={{
|
||||
className: props.connectButtonClassName,
|
||||
}}
|
||||
detailsButton={{
|
||||
className: props.detailsButtonClassName,
|
||||
}}
|
||||
chains={allChainsWithMetadata}
|
||||
detailsModal={{
|
||||
networkSelector: {
|
||||
sections: chainSections,
|
||||
onSwitch(chain) {
|
||||
addRecentlyUsedChainId(chain.id);
|
||||
},
|
||||
renderChain(props) {
|
||||
return (
|
||||
<CustomChainRenderer
|
||||
{...props}
|
||||
openEditChainModal={(c) => {
|
||||
setIsNetworkConfigModalOpen(true);
|
||||
setEditChain(c);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
onCustomClick: () => {
|
||||
setEditChain(undefined);
|
||||
setIsNetworkConfigModalOpen(true);
|
||||
},
|
||||
},
|
||||
renderChain: CustomChainRenderer,
|
||||
onCustomClick: () => {
|
||||
setIsNetworkConfigModalOpen(true);
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
|
||||
<LazyConfigureNetworkModal
|
||||
open={isNetworkConfigModalOpen}
|
||||
onOpenChange={setIsNetworkConfigModalOpen}
|
||||
editChain={editChain}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -162,7 +227,7 @@ function ConnectWalletWelcomeScreen(props: {
|
||||
backgroundColor: props.theme === "dark" ? "#18132f" : "#c7b5f1",
|
||||
backgroundImage: `url("/assets/connect-wallet/welcome-gradient-${props.theme}.png")`,
|
||||
}}
|
||||
className="flex flex-col p-6 h-full bg-cover bg-center bg-no-repeat"
|
||||
className="flex h-full flex-col bg-center bg-cover bg-no-repeat p-6"
|
||||
>
|
||||
<div className="flex flex-grow flex-col justify-center">
|
||||
<div>
|
||||
@@ -183,7 +248,7 @@ function ConnectWalletWelcomeScreen(props: {
|
||||
|
||||
<div className="h-10" />
|
||||
<h2
|
||||
className="text-xl text-center font-semibold"
|
||||
className="text-center font-semibold text-xl"
|
||||
style={{
|
||||
color: fontColor,
|
||||
}}
|
||||
@@ -194,7 +259,7 @@ function ConnectWalletWelcomeScreen(props: {
|
||||
<div className="h-4" />
|
||||
|
||||
<p
|
||||
className="text-center opacity-80 font-semibold"
|
||||
className="text-center font-semibold opacity-80"
|
||||
style={{
|
||||
color: fontColor,
|
||||
}}
|
||||
@@ -205,7 +270,7 @@ function ConnectWalletWelcomeScreen(props: {
|
||||
</div>
|
||||
|
||||
<TrackedAnchorLink
|
||||
className="text-center font-semibold opacity-70 hover:opacity-100 hover:no-underline"
|
||||
className="text-center font-semibold opacity-70 hover:no-underline hover:opacity-100"
|
||||
target="_blank"
|
||||
category="custom-connect-wallet"
|
||||
label="new-to-wallets"
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
import { networkKeys, useDashboardEVMChainId } from "@3rdweb-sdk/react";
|
||||
import {
|
||||
type MutationFunction,
|
||||
type QueryFunction,
|
||||
type QueryKey,
|
||||
type UseMutationOptions,
|
||||
type UseQueryOptions,
|
||||
type UseQueryResult,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { useCallback } from "react";
|
||||
|
||||
export function useQueryWithNetwork<
|
||||
TQueryFnData = unknown,
|
||||
TError = unknown,
|
||||
TData = TQueryFnData,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
queryKey: TQueryKey,
|
||||
queryFn: QueryFunction<TQueryFnData, TQueryKey>,
|
||||
options?: Omit<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
>,
|
||||
): UseQueryResult<TData, TError> {
|
||||
const activeChainId = useDashboardEVMChainId();
|
||||
|
||||
const mergedOptions: Omit<
|
||||
UseQueryOptions<TQueryFnData, TError, TData, TQueryKey>,
|
||||
"queryKey" | "queryFn"
|
||||
> = {
|
||||
...options,
|
||||
enabled: !!(activeChainId && options?.enabled),
|
||||
};
|
||||
|
||||
const combinedQueryKey = (
|
||||
networkKeys.chain(activeChainId) as readonly unknown[]
|
||||
).concat(queryKey) as unknown as TQueryKey;
|
||||
|
||||
return useQuery({
|
||||
...mergedOptions,
|
||||
queryKey: combinedQueryKey,
|
||||
queryFn,
|
||||
});
|
||||
}
|
||||
export function useMutationWithInvalidate<
|
||||
TData = unknown,
|
||||
TError = unknown,
|
||||
TVariables = void,
|
||||
TContext = unknown,
|
||||
TQueryKey extends QueryKey = QueryKey,
|
||||
>(
|
||||
mutationFn: MutationFunction<TData, TVariables>,
|
||||
options?: Omit<
|
||||
UseMutationOptions<TData, TError, TVariables, TContext>,
|
||||
"mutationFn" | "onSuccess"
|
||||
> & {
|
||||
onSuccess?: (
|
||||
data: TData,
|
||||
variables: TVariables,
|
||||
context: TContext | undefined,
|
||||
wrapCacheKeys: (
|
||||
cacheKeysToInvalidate: TQueryKey[],
|
||||
// biome-ignore lint/suspicious/noConfusingVoidType: FIX ME
|
||||
) => Promise<void[]>,
|
||||
) => Promise<unknown> | undefined;
|
||||
},
|
||||
) {
|
||||
const activeChainId = useDashboardEVMChainId();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const invalidate = useCallback(
|
||||
(cacheKeysToInvalidate: TQueryKey[]) => {
|
||||
return Promise.all(
|
||||
cacheKeysToInvalidate.map((cacheKey) => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: (
|
||||
networkKeys.chain(activeChainId) as readonly unknown[]
|
||||
).concat(cacheKey) as unknown[],
|
||||
});
|
||||
}),
|
||||
);
|
||||
},
|
||||
[queryClient, activeChainId],
|
||||
);
|
||||
|
||||
return useMutation({
|
||||
...options,
|
||||
mutationFn,
|
||||
onSuccess: (...args) => {
|
||||
if (options?.onSuccess) {
|
||||
options.onSuccess(...args, invalidate);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import invariant from "tiny-invariant";
|
||||
|
||||
export type EVMContractInfo = {
|
||||
// using null instead of undefined here so that this type can be JSON stringified
|
||||
@@ -14,74 +6,3 @@ export type EVMContractInfo = {
|
||||
chainSlug: string;
|
||||
contractAddress: string;
|
||||
};
|
||||
|
||||
type SetEVMContractInfo = (info: EVMContractInfo) => void;
|
||||
|
||||
const EVMContractInfoContext = createContext<EVMContractInfo | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const SetEVMContractInfoContext = createContext<SetEVMContractInfo | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
// TODO let's move this out of here eventually
|
||||
export function EVMContractInfoProvider(props: {
|
||||
children: React.ReactNode;
|
||||
value?: EVMContractInfo;
|
||||
}) {
|
||||
const [value, setValue] = useState<EVMContractInfo | undefined>(props.value);
|
||||
|
||||
// keep the state in sync with the props.value
|
||||
// this is required when route changes and page goes from fallback to actual page
|
||||
|
||||
const _setValue = useCallback((info?: EVMContractInfo) => {
|
||||
setValue((prevValue) => {
|
||||
if (
|
||||
prevValue?.chainSlug === info?.chainSlug &&
|
||||
prevValue?.contractAddress === info?.contractAddress &&
|
||||
info?.chain === null
|
||||
) {
|
||||
return prevValue;
|
||||
}
|
||||
return info;
|
||||
});
|
||||
}, []);
|
||||
|
||||
// no better way right now - this whole file is going away with RSC migration
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
_setValue(props.value);
|
||||
}, [_setValue, props.value]);
|
||||
// when we move from rendering the fallback page to the actual page, props.value is correct value but state lags behind with undefined value
|
||||
// in that case we use the props.value instead of providing undefined and update the state with effect afterwards
|
||||
// this only happens ONCE, when the page is not yet created on server. once it is created and cached. value will always be defined
|
||||
const providedValue = value || props.value;
|
||||
|
||||
return (
|
||||
<EVMContractInfoContext.Provider value={providedValue}>
|
||||
<SetEVMContractInfoContext.Provider value={_setValue}>
|
||||
{props.children}
|
||||
</SetEVMContractInfoContext.Provider>
|
||||
</EVMContractInfoContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useEVMContractInfo() {
|
||||
return useContext(EVMContractInfoContext);
|
||||
}
|
||||
|
||||
export function useSetEVMContractInfo() {
|
||||
const setter = useContext(SetEVMContractInfoContext);
|
||||
invariant(
|
||||
setter,
|
||||
"useSetEVMContractInfo must be used inside EVMContractInfoProvider",
|
||||
);
|
||||
return setter;
|
||||
}
|
||||
|
||||
// for EVM - get network info from context
|
||||
export function useDashboardEVMChainId() {
|
||||
const contractInfo = useEVMContractInfo();
|
||||
return contractInfo?.chain?.chainId;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,8 @@ import {
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { THIRDWEB_ANALYTICS_API_HOST, THIRDWEB_API_HOST } from "constants/urls";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
import invariant from "tiny-invariant";
|
||||
import { accountKeys, apiKeys, authorizedWallets } from "../cache-keys";
|
||||
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
|
||||
import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
// FIXME: We keep repeating types, API server should provide them
|
||||
@@ -68,6 +66,7 @@ export type Account = {
|
||||
paymentFailureCode: string;
|
||||
serviceCutoffDate: string;
|
||||
}[];
|
||||
// TODO - add image URL
|
||||
};
|
||||
|
||||
interface UpdateAccountInput {
|
||||
@@ -479,8 +478,8 @@ export function useUpdateAccountPlan(waitForWebhook?: boolean) {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: { plan: string; feedback?: string }) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: { plan: string; feedback?: string }) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/plan`, {
|
||||
@@ -506,27 +505,26 @@ export function useUpdateAccountPlan(waitForWebhook?: boolean) {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
onSuccess: async () => {
|
||||
return Promise.all([
|
||||
// invalidate usage data as limits are different
|
||||
await queryClient.invalidateQueries({
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
|
||||
return queryClient.invalidateQueries({
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.usage(user?.address as string),
|
||||
});
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateNotifications() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateAccountNotificationsInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateAccountNotificationsInput) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/notifications`, {
|
||||
@@ -545,14 +543,12 @@ export function useUpdateNotifications() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateBillingSession(enabled = false) {
|
||||
@@ -589,8 +585,8 @@ export function useConfirmEmail() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: ConfirmEmailInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: ConfirmEmailInput) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/confirmEmail`, {
|
||||
@@ -609,31 +605,29 @@ export function useConfirmEmail() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: async () => {
|
||||
// invalidate related cache, since could be relinking account
|
||||
return Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.usage(user?.address as string),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
}),
|
||||
]);
|
||||
},
|
||||
onSuccess: async () => {
|
||||
// invalidate related cache, since could be relinking account
|
||||
return Promise.all([
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.usage(user?.address as string),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
}),
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useResendEmailConfirmation() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(
|
||||
@@ -655,22 +649,20 @@ export function useResendEmailConfirmation() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreatePaymentMethod() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (paymentMethodId: string) => {
|
||||
return useMutation({
|
||||
mutationFn: async (paymentMethodId: string) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/account/paymentMethod`, {
|
||||
@@ -691,14 +683,12 @@ export function useCreatePaymentMethod() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: accountKeys.me(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useApiKeys() {
|
||||
@@ -761,8 +751,8 @@ export function useUpdateApiKey() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateKeyInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateKeyInput) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${input.id}`, {
|
||||
@@ -782,22 +772,20 @@ export function useUpdateApiKey() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useRevokeApiKey() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (id: string) => {
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/keys/${id}/revoke`, {
|
||||
@@ -816,14 +804,12 @@ export function useRevokeApiKey() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: apiKeys.keys(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export const usePolicies = (serviceId?: string) => {
|
||||
@@ -852,8 +838,11 @@ export const usePolicies = (serviceId?: string) => {
|
||||
|
||||
export const useUpdatePolicies = () => {
|
||||
const queryClient = useQueryClient();
|
||||
return useMutationWithInvalidate(
|
||||
async (input: { serviceId: string; data: ApiKeyServicePolicy }) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: {
|
||||
serviceId: string;
|
||||
data: ApiKeyServicePolicy;
|
||||
}) => {
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/policies`, {
|
||||
method: "POST",
|
||||
|
||||
@@ -871,22 +860,20 @@ export const useUpdatePolicies = () => {
|
||||
}
|
||||
return json.data as ApiKeyServicePolicy;
|
||||
},
|
||||
{
|
||||
onSuccess: (_, variables) => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: ["policies", variables.serviceId],
|
||||
});
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: ["policies", variables.serviceId],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export function useRevokeAuthorizedWallet() {
|
||||
const { user } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (variables: { authorizedWalletId: string }) => {
|
||||
return useMutation({
|
||||
mutationFn: async (variables: { authorizedWalletId: string }) => {
|
||||
invariant(user?.address, "walletAddress is required");
|
||||
|
||||
const { authorizedWalletId } = variables;
|
||||
@@ -910,16 +897,12 @@ export function useRevokeAuthorizedWallet() {
|
||||
|
||||
return json.data;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: authorizedWallets.authorizedWallets(
|
||||
user?.address as string,
|
||||
),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: authorizedWallets.authorizedWallets(user?.address as string),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useAuthorizedWallets() {
|
||||
@@ -947,28 +930,3 @@ export function useAuthorizedWallets() {
|
||||
gcTime: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
async function fetchChainsFromApi() {
|
||||
// always fetch from prod for chains for now
|
||||
// TODO: re-visit this
|
||||
const res = await fetch("https://api.thirdweb.com/v1/chains");
|
||||
const json = await res.json();
|
||||
|
||||
if (json.error) {
|
||||
throw new Error(json.error.message);
|
||||
}
|
||||
|
||||
return json.data as ChainMetadata[];
|
||||
}
|
||||
|
||||
export function useApiChains() {
|
||||
return useQuery({
|
||||
queryKey: ["all-chains"],
|
||||
queryFn: () => {
|
||||
return fetchChainsFromApi();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,41 +14,52 @@ import { getContractMetadata, name, symbol } from "thirdweb/extensions/common";
|
||||
export function useDashboardContractMetadata(contract: ThirdwebContract) {
|
||||
return useQuery({
|
||||
queryKey: ["contract-metadata-header", contract.chain.id, contract.address],
|
||||
queryFn: async () => {
|
||||
const [contractMetadata, _name, _symbol, compilerMetadata] =
|
||||
await Promise.all([
|
||||
getContractMetadata({ contract }).catch(() => ({
|
||||
image: "",
|
||||
name: "",
|
||||
symbol: "",
|
||||
})),
|
||||
name({ contract }).catch(() => ""),
|
||||
symbol({ contract }).catch(() => ""),
|
||||
getCompilerMetadata(contract).catch(() => undefined),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Handle the case where the contract.thirdweb.com endpoint returns a very long name
|
||||
* example url: https://contract.thirdweb.com/metadata/324/0x66E298335c5992d599132582De8eaAF75348e6e6
|
||||
* In that case, we split the solidity name on ":" and take the second part
|
||||
*/
|
||||
if (compilerMetadata?.name.includes(":")) {
|
||||
const _name = compilerMetadata.name.split(":")[1];
|
||||
if (_name) {
|
||||
compilerMetadata.name = compilerMetadata.name.split(":")[1];
|
||||
}
|
||||
}
|
||||
|
||||
const contractName =
|
||||
contractMetadata?.name || _name || compilerMetadata?.name || "";
|
||||
const contractSymbol = contractMetadata?.symbol || _symbol || "";
|
||||
|
||||
return {
|
||||
name: contractName,
|
||||
symbol: contractSymbol,
|
||||
image: contractMetadata.image || "",
|
||||
contractType: compilerMetadata?.name || "",
|
||||
};
|
||||
},
|
||||
queryFn: () => fetchDashboardContractMetadata(contract),
|
||||
});
|
||||
}
|
||||
|
||||
type DashboardContractMetadata = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
image: string;
|
||||
contractType: string;
|
||||
};
|
||||
|
||||
export async function fetchDashboardContractMetadata(
|
||||
contract: ThirdwebContract,
|
||||
): Promise<DashboardContractMetadata> {
|
||||
const [contractMetadata, _name, _symbol, compilerMetadata] =
|
||||
await Promise.all([
|
||||
getContractMetadata({ contract }).catch(() => ({
|
||||
image: "",
|
||||
name: "",
|
||||
symbol: "",
|
||||
})),
|
||||
name({ contract }).catch(() => ""),
|
||||
symbol({ contract }).catch(() => ""),
|
||||
getCompilerMetadata(contract).catch(() => undefined),
|
||||
]);
|
||||
|
||||
/**
|
||||
* Handle the case where the contract.thirdweb.com endpoint returns a very long name
|
||||
* example url: https://contract.thirdweb.com/metadata/324/0x66E298335c5992d599132582De8eaAF75348e6e6
|
||||
* In that case, we split the solidity name on ":" and take the second part
|
||||
*/
|
||||
if (compilerMetadata?.name.includes(":")) {
|
||||
const _name = compilerMetadata.name.split(":")[1];
|
||||
if (_name) {
|
||||
compilerMetadata.name = compilerMetadata.name.split(":")[1];
|
||||
}
|
||||
}
|
||||
|
||||
const contractName =
|
||||
contractMetadata?.name || _name || compilerMetadata?.name || "";
|
||||
const contractSymbol = contractMetadata?.symbol || _symbol || "";
|
||||
|
||||
return {
|
||||
name: contractName,
|
||||
symbol: contractSymbol,
|
||||
image: contractMetadata.image || "",
|
||||
contractType: compilerMetadata?.name || "",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import type { ResultItem } from "components/engine/system-metrics/components/StatusCodes";
|
||||
import { THIRDWEB_API_HOST } from "constants/urls";
|
||||
@@ -5,7 +7,6 @@ import { useState } from "react";
|
||||
import { useActiveAccount, useActiveWalletChain } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { engineKeys } from "../cache-keys";
|
||||
import { useMutationWithInvalidate } from "./query/useQueryWithNetwork";
|
||||
import { useLoggedInUser } from "./useLoggedInUser";
|
||||
|
||||
export type EngineTier = "STARTER" | "PREMIUM" | "ENTERPRISE";
|
||||
@@ -17,7 +18,6 @@ export type EngineInstance = {
|
||||
name: string;
|
||||
url: string;
|
||||
lastAccessedAt: string;
|
||||
cloudDeployedAt?: string;
|
||||
status:
|
||||
| "active"
|
||||
| "pending"
|
||||
@@ -25,6 +25,7 @@ export type EngineInstance = {
|
||||
| "deploying"
|
||||
| "paymentFailed"
|
||||
| "deploymentFailed";
|
||||
deploymentId?: string;
|
||||
};
|
||||
|
||||
// Not checking for null token because the token is required the tanstack useQuery hook
|
||||
@@ -188,23 +189,26 @@ export function useEngineLatestVersion() {
|
||||
|
||||
interface UpdateVersionInput {
|
||||
engineId: string;
|
||||
serverVersion: string;
|
||||
}
|
||||
|
||||
export function useEngineUpdateVersion() {
|
||||
export function useEngineUpdateServerVersion() {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateVersionInput) => {
|
||||
invariant(input.engineId, "engineId is required");
|
||||
|
||||
const res = await fetch(`${THIRDWEB_API_HOST}/v1/engine/update-version`, {
|
||||
method: "POST",
|
||||
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v2/engine/${input.engineId}/infrastructure`,
|
||||
{
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
serverVersion: input.serverVersion,
|
||||
}),
|
||||
},
|
||||
body: JSON.stringify({
|
||||
engineId: input.engineId,
|
||||
}),
|
||||
});
|
||||
);
|
||||
// we never use the response body
|
||||
res.body?.cancel();
|
||||
if (!res.ok) {
|
||||
@@ -570,8 +574,8 @@ export function useEngineAddKeypair(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: AddKeypairInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: AddKeypairInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/keypair/add`, {
|
||||
@@ -587,14 +591,12 @@ export function useEngineAddKeypair(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.keypairs(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.keypairs(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RemoveKeypairInput = {
|
||||
@@ -605,8 +607,8 @@ export function useEngineRemoveKeypair(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RemoveKeypairInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RemoveKeypairInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/keypair/remove`, {
|
||||
@@ -622,14 +624,12 @@ export function useEngineRemoveKeypair(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.keypairs(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.keypairs(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type EngineRelayer = {
|
||||
@@ -672,8 +672,8 @@ export function useEngineCreateRelayer(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: CreateRelayerInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateRelayerInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}relayer/create`, {
|
||||
@@ -689,14 +689,12 @@ export function useEngineCreateRelayer(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RevokeRelayerInput = {
|
||||
@@ -707,8 +705,8 @@ export function useEngineRevokeRelayer(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RevokeRelayerInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RevokeRelayerInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}relayer/revoke`, {
|
||||
@@ -724,14 +722,12 @@ export function useEngineRevokeRelayer(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type UpdateRelayerInput = {
|
||||
@@ -747,8 +743,8 @@ export function useEngineUpdateRelayer(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateRelayerInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateRelayerInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}relayer/update`, {
|
||||
@@ -764,14 +760,12 @@ export function useEngineUpdateRelayer(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.relayers(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export interface EngineWebhook {
|
||||
@@ -827,8 +821,8 @@ export function useEngineSetWalletConfig(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: SetWalletConfigInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: SetWalletConfigInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}configuration/wallets`, {
|
||||
@@ -844,14 +838,12 @@ export function useEngineSetWalletConfig(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletConfig(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.walletConfig(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateBackendWalletInput = {
|
||||
@@ -862,8 +854,8 @@ export function useEngineCreateBackendWallet(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: CreateBackendWalletInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateBackendWalletInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}backend-wallet/create`, {
|
||||
@@ -879,14 +871,12 @@ export function useEngineCreateBackendWallet(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
interface UpdateBackendWalletInput {
|
||||
@@ -898,8 +888,8 @@ export function useEngineUpdateBackendWallet(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateBackendWalletInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateBackendWalletInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}backend-wallet/update`, {
|
||||
@@ -915,14 +905,12 @@ export function useEngineUpdateBackendWallet(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type ImportBackendWalletInput =
|
||||
@@ -949,8 +937,8 @@ export function useEngineImportBackendWallet(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: ImportBackendWalletInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: ImportBackendWalletInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}backend-wallet/import`, {
|
||||
@@ -966,22 +954,20 @@ export function useEngineImportBackendWallet(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.backendWallets(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function useEngineGrantPermissions(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: EngineAdmin) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: EngineAdmin) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/permissions/grant`, {
|
||||
@@ -997,14 +983,12 @@ export function useEngineGrantPermissions(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.permissions(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.permissions(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RevokePermissionsInput = {
|
||||
@@ -1015,8 +999,8 @@ export function useEngineRevokePermissions(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RevokePermissionsInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RevokePermissionsInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/permissions/revoke`, {
|
||||
@@ -1032,14 +1016,12 @@ export function useEngineRevokePermissions(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.permissions(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.permissions(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type CreateAccessTokenResponse = AccessToken & {
|
||||
@@ -1050,8 +1032,8 @@ export function useEngineCreateAccessToken(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async () => {
|
||||
return useMutation({
|
||||
mutationFn: async () => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/access-tokens/create`, {
|
||||
@@ -1067,14 +1049,12 @@ export function useEngineCreateAccessToken(instance: string) {
|
||||
|
||||
return json.result as CreateAccessTokenResponse;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RevokeAccessTokenInput = {
|
||||
@@ -1085,8 +1065,8 @@ export function useEngineRevokeAccessToken(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RevokeAccessTokenInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RevokeAccessTokenInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/access-tokens/revoke`, {
|
||||
@@ -1102,14 +1082,12 @@ export function useEngineRevokeAccessToken(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type UpdateAccessTokenInput = {
|
||||
@@ -1121,8 +1099,8 @@ export function useEngineUpdateAccessToken(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: UpdateAccessTokenInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: UpdateAccessTokenInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}auth/access-tokens/update`, {
|
||||
@@ -1138,14 +1116,12 @@ export function useEngineUpdateAccessToken(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.accessTokens(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export type CreateWebhookInput = {
|
||||
@@ -1158,8 +1134,8 @@ export function useEngineCreateWebhook(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: CreateWebhookInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateWebhookInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}webhooks/create`, {
|
||||
@@ -1175,14 +1151,12 @@ export function useEngineCreateWebhook(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.webhooks(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.webhooks(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type RevokeWebhookInput = {
|
||||
@@ -1193,8 +1167,8 @@ export function useEngineRevokeWebhook(instance: string) {
|
||||
const token = useLoggedInUser().user?.jwt ?? null;
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutationWithInvalidate(
|
||||
async (input: RevokeWebhookInput) => {
|
||||
return useMutation({
|
||||
mutationFn: async (input: RevokeWebhookInput) => {
|
||||
invariant(instance, "instance is required");
|
||||
|
||||
const res = await fetch(`${instance}webhooks/revoke`, {
|
||||
@@ -1210,14 +1184,12 @@ export function useEngineRevokeWebhook(instance: string) {
|
||||
|
||||
return json.result;
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.webhooks(instance),
|
||||
});
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.webhooks(instance),
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
type SendTokensInput = {
|
||||
@@ -1539,3 +1511,173 @@ export function useEngineSystemMetrics(engineId: string) {
|
||||
refetchInterval: 5_000,
|
||||
});
|
||||
}
|
||||
|
||||
export interface EngineAlertRule {
|
||||
id: string;
|
||||
title: string;
|
||||
// A unique string identifying the alert.
|
||||
// Note the "." which allows `subscriptionRoutes` to use wildcards in notificationChannel.
|
||||
// Example: "alert.sla-5xx-99"
|
||||
routingKey: string;
|
||||
description: string;
|
||||
createdAt: Date;
|
||||
pausedAt: Date | null;
|
||||
}
|
||||
|
||||
export function useEngineAlertRules(engineId: string) {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery({
|
||||
queryKey: engineKeys.alertRules(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/alert-rules`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as EngineAlertRule[];
|
||||
},
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export interface EngineAlert {
|
||||
id: string;
|
||||
alertRuleId: string;
|
||||
status: "pending" | "firing" | "resolved";
|
||||
startsAt: Date;
|
||||
endsAt: Date | null;
|
||||
}
|
||||
|
||||
export function useEngineAlerts(engineId: string, limit: number, offset = 0) {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery({
|
||||
queryKey: engineKeys.alerts(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/alerts?limit=${limit}&offset=${offset}`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as EngineAlert[];
|
||||
},
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export const EngineNotificationChannelTypeConfig = {
|
||||
slack: {
|
||||
display: "Slack",
|
||||
valueDisplay: "Slack Webhook URL",
|
||||
},
|
||||
email: {
|
||||
display: "Email",
|
||||
valueDisplay: "Email Address",
|
||||
},
|
||||
} as const;
|
||||
|
||||
type EngineNotificationChannelType =
|
||||
keyof typeof EngineNotificationChannelTypeConfig;
|
||||
|
||||
export type EngineNotificationChannel = {
|
||||
id: string;
|
||||
type: EngineNotificationChannelType;
|
||||
value: string;
|
||||
createdAt: Date;
|
||||
pausedAt: Date | null;
|
||||
// A list of routingKeys to listen to. Supports wildcards.
|
||||
// Example: [ 'alert.sla-5xx-99' ] or [ 'alert.*' ] will both notify when
|
||||
// the alert with routingKey `alert.sla-5xx-99` triggers.
|
||||
subscriptionRoutes: string[];
|
||||
};
|
||||
|
||||
export function useEngineNotificationChannels(engineId: string) {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
|
||||
return useQuery({
|
||||
queryKey: engineKeys.notificationChannels(engineId),
|
||||
queryFn: async () => {
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`,
|
||||
{ method: "GET" },
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as EngineNotificationChannel[];
|
||||
},
|
||||
enabled: isLoggedIn,
|
||||
});
|
||||
}
|
||||
|
||||
export interface CreateNotificationChannelInput {
|
||||
subscriptionRoutes: string[];
|
||||
type: "slack" | "email"; // TODO: Add others when implemented.
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function useEngineCreateNotificationChannel(engineId: string) {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (input: CreateNotificationChannelInput) => {
|
||||
invariant(isLoggedIn, "Must be logged in.");
|
||||
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(input),
|
||||
},
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as EngineNotificationChannel;
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.notificationChannels(engineId),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useEngineDeleteNotificationChannel(engineId: string) {
|
||||
const { isLoggedIn } = useLoggedInUser();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (notificationChannelId: string) => {
|
||||
invariant(isLoggedIn, "Must be logged in.");
|
||||
|
||||
const res = await fetch(
|
||||
`${THIRDWEB_API_HOST}/v1/engine/${engineId}/notification-channels/${notificationChannelId}`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
if (!res.ok) {
|
||||
throw new Error(`Unexpected status ${res.status}: ${await res.text()}`);
|
||||
}
|
||||
res.body?.cancel();
|
||||
},
|
||||
onSuccess: () => {
|
||||
return queryClient.invalidateQueries({
|
||||
queryKey: engineKeys.notificationChannels(engineId),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { useFavouriteChainIds } from "app/(dashboard)/(chain)/components/client/star-button";
|
||||
import { useSupportedChains } from "hooks/chains/configureChains";
|
||||
import { useMemo } from "react";
|
||||
import type { ChainMetadata } from "thirdweb/chains";
|
||||
|
||||
export function useFavoriteChains() {
|
||||
const allChains = useSupportedChains();
|
||||
const favChainsQuery = useFavouriteChainIds();
|
||||
|
||||
const data = useMemo(() => {
|
||||
if (favChainsQuery.data) {
|
||||
const _chains: ChainMetadata[] = [];
|
||||
// biome-ignore lint/complexity/noForEach: FIXME
|
||||
favChainsQuery.data.forEach((chainId) => {
|
||||
const chain = allChains.find((c) => String(c.chainId) === chainId);
|
||||
if (chain) {
|
||||
_chains.push(chain);
|
||||
}
|
||||
});
|
||||
|
||||
return _chains;
|
||||
}
|
||||
|
||||
return [] as ChainMetadata[];
|
||||
}, [favChainsQuery.data, allChains]);
|
||||
|
||||
return {
|
||||
isLoading: favChainsQuery.isLoading,
|
||||
data,
|
||||
};
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { isLoginRequired } from "@/constants/auth";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
@@ -17,7 +19,7 @@ declare global {
|
||||
}
|
||||
|
||||
export function useLoggedInUser(): {
|
||||
isLoading: boolean;
|
||||
isPending: boolean;
|
||||
isLoggedIn: boolean;
|
||||
user: { address: string; jwt?: string } | null;
|
||||
} {
|
||||
@@ -82,7 +84,7 @@ export function useLoggedInUser(): {
|
||||
// if we are "disconnected" we are not logged in
|
||||
if (connectionStatus === "disconnected") {
|
||||
return {
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
};
|
||||
@@ -91,7 +93,7 @@ export function useLoggedInUser(): {
|
||||
// if we are still connecting, we are "loading"
|
||||
if (connectionStatus === "connecting") {
|
||||
return {
|
||||
isLoading: true,
|
||||
isPending: true,
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
};
|
||||
@@ -100,7 +102,7 @@ export function useLoggedInUser(): {
|
||||
// same if we do not yet have a path
|
||||
if (!pathname) {
|
||||
return {
|
||||
isLoading: true,
|
||||
isPending: true,
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
};
|
||||
@@ -109,7 +111,7 @@ export function useLoggedInUser(): {
|
||||
// if we do not have an address we are not logged in
|
||||
if (!connectedAddress) {
|
||||
return {
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
isLoggedIn: false,
|
||||
user: null,
|
||||
};
|
||||
@@ -118,7 +120,7 @@ export function useLoggedInUser(): {
|
||||
// if we are not on a logged in path, we can simply return the connected address
|
||||
if (!isLoginRequired(pathname)) {
|
||||
return {
|
||||
isLoading: false,
|
||||
isPending: false,
|
||||
isLoggedIn: true,
|
||||
user: { address: connectedAddress },
|
||||
};
|
||||
@@ -126,7 +128,7 @@ export function useLoggedInUser(): {
|
||||
|
||||
// otherwise we return the query data
|
||||
return {
|
||||
isLoading: query.isLoading,
|
||||
isPending: query.isPending,
|
||||
isLoggedIn: query.data ? query.data.isLoggedIn : false,
|
||||
user: query.data?.isLoggedIn
|
||||
? { address: connectedAddress, jwt: query.data.jwt }
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
import type { BasicContract } from "contract-ui/types/types";
|
||||
import { getAllMultichainRegistry } from "dashboard-extensions/common/read/getAllMultichainRegistry";
|
||||
import { useAllChainsData } from "hooks/chains/allChains";
|
||||
import { useSupportedChainsRecord } from "hooks/chains/configureChains";
|
||||
import { useMemo } from "react";
|
||||
import { sendAndConfirmTransaction } from "thirdweb";
|
||||
import { remove } from "thirdweb/extensions/thirdweb";
|
||||
@@ -42,7 +41,8 @@ export const useAllContractList = (
|
||||
) => {
|
||||
const multiChainQuery = useMultiChainRegContractList(walletAddress);
|
||||
|
||||
const configuredChainsRecord = useSupportedChainsRecord();
|
||||
// TODO - instead of using ALL chains, fetch only the ones used here
|
||||
const { idToChain } = useAllChainsData();
|
||||
const contractList = useMemo(() => {
|
||||
const data = multiChainQuery.data || [];
|
||||
|
||||
@@ -51,14 +51,12 @@ export const useAllContractList = (
|
||||
|
||||
// biome-ignore lint/complexity/noForEach: FIXME
|
||||
data.forEach((net) => {
|
||||
if (net.chainId in configuredChainsRecord) {
|
||||
const chainRecord = configuredChainsRecord[net.chainId];
|
||||
if (chainRecord.status !== "deprecated") {
|
||||
if (chainRecord.testnet) {
|
||||
testnets.push(net);
|
||||
} else {
|
||||
mainnets.push(net);
|
||||
}
|
||||
const chn = idToChain.get(net.chainId);
|
||||
if (chn && chn.status !== "deprecated") {
|
||||
if (chn.testnet) {
|
||||
testnets.push(net);
|
||||
} else {
|
||||
mainnets.push(net);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -71,7 +69,7 @@ export const useAllContractList = (
|
||||
|
||||
testnets.sort((a, b) => a.chainId - b.chainId);
|
||||
return mainnets.concat(testnets);
|
||||
}, [multiChainQuery.data, onlyMainnet, configuredChainsRecord]);
|
||||
}, [multiChainQuery.data, onlyMainnet, idToChain]);
|
||||
|
||||
return {
|
||||
...multiChainQuery,
|
||||
@@ -85,12 +83,10 @@ type RemoveContractParams = {
|
||||
};
|
||||
|
||||
export function useRemoveContractMutation() {
|
||||
const { chainIdToChainRecord } = useAllChainsData();
|
||||
const queryClient = useQueryClient();
|
||||
const account = useActiveAccount();
|
||||
return useMutation({
|
||||
mutationFn: async (data: RemoveContractParams) => {
|
||||
invariant(chainIdToChainRecord, "chains not initialzed yet");
|
||||
invariant(data.chainId, "chainId not provided");
|
||||
invariant(account, "No wallet connected");
|
||||
const { contractAddress, chainId } = data;
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { WalletNFTApiReturn } from "pages/api/wallet/nfts/[chainId]";
|
||||
import { useQueryWithNetwork } from "./query/useQueryWithNetwork";
|
||||
|
||||
export function useWalletNFTs(walletAddress?: string, chainId?: number) {
|
||||
return useQueryWithNetwork(
|
||||
["walletNfts", walletAddress],
|
||||
async () => {
|
||||
export function useWalletNFTs(params: {
|
||||
chainId: number;
|
||||
walletAddress?: string;
|
||||
}) {
|
||||
return useQuery({
|
||||
queryKey: ["walletNfts", params.chainId, params.walletAddress],
|
||||
queryFn: async () => {
|
||||
const response = await fetch(
|
||||
`/api/wallet/nfts/${chainId}?owner=${walletAddress}`,
|
||||
`/api/wallet/nfts/${params.chainId}?owner=${params.walletAddress}`,
|
||||
);
|
||||
return (await response.json()) as WalletNFTApiReturn;
|
||||
},
|
||||
{
|
||||
enabled: !!walletAddress && !!chainId,
|
||||
},
|
||||
);
|
||||
enabled: !!params.walletAddress,
|
||||
});
|
||||
}
|
||||
|
||||
+5
-5
@@ -118,7 +118,7 @@ export function FaucetButton({
|
||||
faucetWalletBalanceQuery.data.value < toUnits("1", 17);
|
||||
|
||||
// loading state
|
||||
if (faucetWalletBalanceQuery.isLoading || canClaimFaucetQuery.isLoading) {
|
||||
if (faucetWalletBalanceQuery.isPending || canClaimFaucetQuery.isPending) {
|
||||
return (
|
||||
<Button variant="outline" className="w-full gap-2">
|
||||
Checking Faucet <Spinner className="size-3" />
|
||||
@@ -129,7 +129,7 @@ export function FaucetButton({
|
||||
// faucet is empty
|
||||
if (isFaucetEmpty) {
|
||||
return (
|
||||
<Button variant="outline" disabled className="w-full !opacity-100 ">
|
||||
<Button variant="outline" disabled className="!opacity-100 w-full ">
|
||||
Faucet is empty right now
|
||||
</Button>
|
||||
);
|
||||
@@ -138,7 +138,7 @@ export function FaucetButton({
|
||||
// Can not claim
|
||||
if (canClaimFaucetQuery.data && canClaimFaucetQuery.data.canClaim === false) {
|
||||
return (
|
||||
<Button variant="outline" className="w-full !opacity-100 " disabled>
|
||||
<Button variant="outline" className="!opacity-100 w-full " disabled>
|
||||
{canClaimFaucetQuery.data.type === "throttle" && (
|
||||
<>
|
||||
Your next claim is available{" "}
|
||||
@@ -163,7 +163,7 @@ export function FaucetButton({
|
||||
|
||||
// eligible to claim and faucet has balance
|
||||
return (
|
||||
<div className="flex flex-col w-full text-center">
|
||||
<div className="flex w-full flex-col text-center">
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full gap-2"
|
||||
@@ -185,7 +185,7 @@ export function FaucetButton({
|
||||
</Button>
|
||||
|
||||
{faucetWalletBalanceQuery.data && (
|
||||
<p className="mt-3 text-xs text-muted-foreground">
|
||||
<p className="mt-3 text-muted-foreground text-xs">
|
||||
{Number(faucetWalletBalanceQuery.data.displayValue).toFixed(3)}{" "}
|
||||
{faucetWalletBalanceQuery.data.symbol} left in the faucet
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ export function PayModalButton(props: { chainId: number; label: string }) {
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent
|
||||
className="md:max-w-[360px] p-0 border-none bg-transparent"
|
||||
className="border-none bg-transparent p-0 md:max-w-[360px]"
|
||||
dialogOverlayClassName="backdrop-blur-lg"
|
||||
dialogCloseClassName="focus:ring-0"
|
||||
>
|
||||
|
||||
+1
-1
@@ -44,7 +44,7 @@ export const AddChainToWallet: React.FC<AddChainToWalletProps> = (props) => {
|
||||
>
|
||||
<Button
|
||||
disabled={disabled}
|
||||
className="gap-2 w-full"
|
||||
className="w-full gap-2"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
// Connct directly to this chain
|
||||
|
||||
+6
-6
@@ -83,7 +83,7 @@ export function ChainLiveStats(props: { rpc: string }) {
|
||||
textToCopy={props.rpc}
|
||||
copyIconPosition="right"
|
||||
variant="ghost"
|
||||
className="px-2 py-1 -translate-x-2 text-base"
|
||||
className="-translate-x-2 px-2 py-1 text-base"
|
||||
iconClassName="size-4"
|
||||
/>
|
||||
</div>
|
||||
@@ -96,7 +96,7 @@ export function ChainLiveStats(props: { rpc: string }) {
|
||||
) : stats.data ? (
|
||||
<p className="fade-in-0 animate-in">{stats.data.latency}ms</p>
|
||||
) : (
|
||||
<div className="flex py-1 h-[28px] w-[70px]">
|
||||
<div className="flex h-[28px] w-[70px] py-1">
|
||||
<Skeleton className="h-full w-full" />
|
||||
</div>
|
||||
)}
|
||||
@@ -109,7 +109,7 @@ export function ChainLiveStats(props: { rpc: string }) {
|
||||
) : stats.data ? (
|
||||
<p className="fade-in-0 animate-in">{stats.data.blockNumber}</p>
|
||||
) : (
|
||||
<div className="flex py-1 h-[28px] w-[140px]">
|
||||
<div className="flex h-[28px] w-[140px] py-1">
|
||||
<Skeleton className="h-full w-full" />
|
||||
</div>
|
||||
)}
|
||||
@@ -120,10 +120,10 @@ export function ChainLiveStats(props: { rpc: string }) {
|
||||
|
||||
function PulseDot() {
|
||||
return (
|
||||
<ToolTipLabel label={"Live Data"}>
|
||||
<ToolTipLabel label="Live Data">
|
||||
<span className="relative flex size-2">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-sky-400 opacity-75" />
|
||||
<span className="relative inline-flex rounded-full size-2 bg-primary" />
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-sky-400 opacity-75" />
|
||||
<span className="relative inline-flex size-2 rounded-full bg-primary" />
|
||||
</span>
|
||||
</ToolTipLabel>
|
||||
);
|
||||
|
||||
+8
-8
@@ -12,28 +12,28 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Buy Funds" />
|
||||
<div className="border border-border px-4 py-10 rounded-lg flex justify-center bg-muted/30">
|
||||
<div className="max-w-[520px] flex flex-col items-center ">
|
||||
<div className="flex justify-center rounded-lg border border-border bg-muted/30 px-4 py-10">
|
||||
<div className="flex max-w-[520px] flex-col items-center ">
|
||||
<div className="flex items-center">
|
||||
<ChainIcon
|
||||
className="size-12 -mr-2 rounded-full border p-1"
|
||||
className="-mr-2 size-12 rounded-full border p-1"
|
||||
iconUrl={props.chain.icon?.url}
|
||||
/>
|
||||
<CreditCardIcon
|
||||
bg="hsl(var(--background))"
|
||||
className="size-12 -ml-2"
|
||||
className="-ml-2 size-12"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<h2 className="text-lg tracking-tight font-semibold text-center px-4">
|
||||
<h2 className="px-4 text-center font-semibold text-lg tracking-tight">
|
||||
Buy Funds on {sanitizedChainName} using thirdweb Pay
|
||||
</h2>
|
||||
|
||||
<div className="h-2" />
|
||||
|
||||
<p className="text-muted-foreground text-sm max-w-[520px] text-center px-4">
|
||||
<p className="max-w-[520px] px-4 text-center text-muted-foreground text-sm">
|
||||
Instantly onramp, bridge & swap into {sanitizedChainName} with any
|
||||
token.
|
||||
</p>
|
||||
@@ -49,9 +49,9 @@ export function BuyFundsSection(props: { chain: ChainMetadata }) {
|
||||
|
||||
<Link
|
||||
href="https://portal.thirdweb.com/connect/pay/overview"
|
||||
className="inline-flex gap-1.5 items-center text-sm text-muted-foreground hover:text-foreground"
|
||||
className="inline-flex items-center gap-1.5 text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
Learn more about thirdweb Pay{" "}
|
||||
Learn more about thirdweb Pay
|
||||
<ExternalLinkIcon className="size-3" />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
+2
-2
@@ -11,7 +11,7 @@ export function ChainOverviewSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Chain Overview" />
|
||||
<div className="grid grid-cols-1 gap-6 lg:gap-8 md:grid-cols-2 lg:grid-cols-3 border rounded-lg p-4 md:p-6 bg-muted/50">
|
||||
<div className="grid grid-cols-1 gap-6 rounded-lg border bg-muted/50 p-4 md:grid-cols-2 md:p-6 lg:grid-cols-3 lg:gap-8">
|
||||
{/* Info */}
|
||||
{chain.infoURL && (
|
||||
<PrimaryInfoItem title="Info">
|
||||
@@ -33,7 +33,7 @@ export function ChainOverviewSection(props: { chain: ChainMetadata }) {
|
||||
textToShow={chain.chainId.toString()}
|
||||
tooltip="Copy Chain ID"
|
||||
variant="ghost"
|
||||
className="-translate-x-2 py-1 px-2 text-base"
|
||||
className="-translate-x-2 px-2 py-1 text-base"
|
||||
iconClassName="size-4"
|
||||
copyIconPosition="right"
|
||||
/>
|
||||
|
||||
+3
-3
@@ -7,9 +7,9 @@ export function ClaimChainSection() {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Claim Chain" />
|
||||
<div className="border p-4 rounded-lg bg-muted/50">
|
||||
<div className="rounded-lg border bg-muted/50 p-4">
|
||||
<p className="mb-1"> This chain page is unclaimed </p>
|
||||
<p className="text-muted-foreground text-sm mb-5">
|
||||
<p className="mb-5 text-muted-foreground text-sm">
|
||||
Are you the owner of this ecosystem? Claim this page to add additional
|
||||
information on this page to help your developers get started!
|
||||
</p>
|
||||
@@ -21,7 +21,7 @@ export function ClaimChainSection() {
|
||||
target="_blank"
|
||||
>
|
||||
<span>Claim Chain</span>
|
||||
<ExternalLinkIcon className="size-4 -mt-[3px]" />
|
||||
<ExternalLinkIcon className="-mt-[3px] size-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+6
-6
@@ -15,25 +15,25 @@ export async function FaucetSection(props: { chain: ChainMetadata }) {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Faucet" />
|
||||
<div className="border border-border px-4 py-10 rounded-lg flex justify-center bg-muted/50">
|
||||
<div className="max-w-[520px] w-full flex flex-col items-center ">
|
||||
<div className="flex justify-center rounded-lg border border-border bg-muted/50 px-4 py-10">
|
||||
<div className="flex w-full max-w-[520px] flex-col items-center ">
|
||||
<div className="flex items-center">
|
||||
<ChainIcon
|
||||
className="size-12 -mr-2 rounded-full border p-1"
|
||||
className="-mr-2 size-12 rounded-full border p-1"
|
||||
iconUrl={props.chain.icon?.url}
|
||||
/>
|
||||
<GiftIcon bg="hsl(var(--background))" className="size-12 -ml-2" />
|
||||
<GiftIcon bg="hsl(var(--background))" className="-ml-2 size-12" />
|
||||
</div>
|
||||
|
||||
<div className="h-6" />
|
||||
|
||||
<h2 className="text-lg tracking-tight font-semibold text-center px-4">
|
||||
<h2 className="px-4 text-center font-semibold text-lg tracking-tight">
|
||||
{sanitizedChainName} Faucet
|
||||
</h2>
|
||||
|
||||
<div className="h-2" />
|
||||
|
||||
<p className="text-muted-foreground text-sm max-w-[520px] text-center px-4">
|
||||
<p className="max-w-[520px] px-4 text-center text-muted-foreground text-sm">
|
||||
Get free {chain.nativeCurrency.symbol} fast and reliably.{" "}
|
||||
{amountToGive} {chain.nativeCurrency.symbol}/day.
|
||||
</p>
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
export function SectionTitle(props: { title: string }) {
|
||||
return (
|
||||
<h2 className="text-xl tracking-tight mb-2 font-semibold">{props.title}</h2>
|
||||
<h2 className="mb-2 font-semibold text-xl tracking-tight">{props.title}</h2>
|
||||
);
|
||||
}
|
||||
|
||||
+7
-8
@@ -20,27 +20,26 @@ export function SupportedProductsSection(props: {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="thirdweb Products" />
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{enabledProducts.map((product) => {
|
||||
return (
|
||||
<div
|
||||
key={product.id}
|
||||
className="border rounded-lg p-4 flex gap-3 relative hover:bg-muted bg-muted/50 transition-colors duration-300 pr-8"
|
||||
className="relative flex gap-3 rounded-lg border bg-muted/50 p-4 pr-8 transition-colors duration-300 hover:bg-muted"
|
||||
>
|
||||
<CircleCheckIcon className="size-5 text-success-text absolute top-4 right-4" />
|
||||
<product.icon className="size-5 shrink-0 mt-0.5" />
|
||||
<CircleCheckIcon className="absolute top-4 right-4 size-5 text-success-text" />
|
||||
<product.icon className="mt-0.5 size-5 shrink-0" />
|
||||
<div>
|
||||
<h3 className="font-medium mb-1.5">
|
||||
<h3 className="mb-1.5 font-medium">
|
||||
<Link
|
||||
href={product.link}
|
||||
className="before:absolute before:inset-0"
|
||||
target="_blank"
|
||||
>
|
||||
{" "}
|
||||
{product.name}{" "}
|
||||
{product.name}
|
||||
</Link>
|
||||
</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{product.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
+10
-9
@@ -1,4 +1,3 @@
|
||||
import { AspectRatio } from "@/components/ui/aspect-ratio";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Link from "next/link";
|
||||
@@ -20,27 +19,29 @@ export function ChainHeader(props: ChainHeaderProps) {
|
||||
// force the banner image to be 4:1 aspect ratio and full-width on mobile devices
|
||||
<div className="flex flex-col">
|
||||
{!props.headerImageUrl && <div className="h-8 md:hidden" />}
|
||||
<AspectRatio
|
||||
ratio={props.headerImageUrl ? 4 : 8}
|
||||
className="border-b border-border max-sm:-mx-4"
|
||||
<div
|
||||
className={cn(
|
||||
"max-sm:-mx-4 relative border-border border-b",
|
||||
props.headerImageUrl ? "aspect-[4/1]" : "aspect-[8/1]",
|
||||
)}
|
||||
>
|
||||
{props.headerImageUrl && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={props.headerImageUrl}
|
||||
alt=""
|
||||
className="object-cover object-center h-full w-full"
|
||||
className="h-full w-full object-cover object-center"
|
||||
/>
|
||||
)}
|
||||
</AspectRatio>
|
||||
</div>
|
||||
{/* below header */}
|
||||
<div className="relative flex flex-row justify-end items-end">
|
||||
<div className="relative flex flex-row items-end justify-end">
|
||||
{/* chain logo */}
|
||||
|
||||
<ChainIcon
|
||||
iconUrl={props.logoUrl}
|
||||
className={cn(
|
||||
"p-2 lg:p-4 absolute top-0 left-0 size-20 lg:size-36 rounded-full -translate-y-[50%] overflow-hidden border border-border bg-muted",
|
||||
"-translate-y-[50%] absolute top-0 left-0 size-20 overflow-hidden rounded-full border border-border bg-muted p-2 lg:size-36 lg:p-4",
|
||||
props.headerImageUrl && "lg:left-4",
|
||||
)}
|
||||
/>
|
||||
@@ -48,7 +49,7 @@ export function ChainHeader(props: ChainHeaderProps) {
|
||||
{/* action group */}
|
||||
<div className="pt-3 lg:pt-6">
|
||||
{/* Desktop only */}
|
||||
<div className="hidden lg:flex flex-row gap-2">
|
||||
<div className="hidden flex-row gap-2 lg:flex">
|
||||
<AddChainToWallet
|
||||
chain={
|
||||
// Do not include chain overrides for chain pages
|
||||
|
||||
+4
-4
@@ -12,18 +12,18 @@ export type ChainCTAProps = {
|
||||
export function ChainCTA(props: ChainCTAProps) {
|
||||
return (
|
||||
<div
|
||||
className="bg-cover bg-center bg-no-repeat rounded-xl border border-border overflow-hidden"
|
||||
className="overflow-hidden rounded-xl border border-border bg-center bg-cover bg-no-repeat"
|
||||
style={{
|
||||
backgroundImage: `url(${props.backgroundImageUrl})`,
|
||||
}}
|
||||
>
|
||||
<div className="grid items-center justify-center gap-6 p-8 md:grid-cols-[1fr_auto] md:gap-12 bg-gradient-to-r from-background to-background/50">
|
||||
<div className="grid items-center justify-center gap-6 bg-gradient-to-r from-background to-background/50 p-8 md:grid-cols-[1fr_auto] md:gap-12">
|
||||
<div className="space-y-4 text-center md:text-left">
|
||||
<h2 className="text-2xl md:text-3xl font-bold tracking-tighter text-foreground md:max-w-[85%]">
|
||||
<h2 className="font-bold text-2xl text-foreground tracking-tighter md:max-w-[85%] md:text-3xl">
|
||||
{props.title}
|
||||
</h2>
|
||||
{props.description && (
|
||||
<p className="max-w-[600px] text-muted-foreground leading-relaxed text-sm md:text-base">
|
||||
<p className="max-w-[600px] text-muted-foreground text-sm leading-relaxed md:text-base">
|
||||
{props.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
+5
-5
@@ -9,21 +9,21 @@ export function ExplorersSection(props: {
|
||||
return (
|
||||
<section>
|
||||
<SectionTitle title="Explorers" />
|
||||
<div className="grid grid-cols-1 gap-3 md:gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<div className="grid grid-cols-1 gap-3 md:grid-cols-2 md:gap-4 lg:grid-cols-3">
|
||||
{props.explorers.map((explorer) => {
|
||||
return (
|
||||
<div
|
||||
key={explorer.url}
|
||||
className="p-4 border rounded-xl relative hover:bg-muted bg-muted/50"
|
||||
className="relative rounded-xl border bg-muted/50 p-4 hover:bg-muted"
|
||||
>
|
||||
<ExternalLinkIcon className="size-4 absolute top-4 right-4 text-muted-foreground" />
|
||||
<h3 className="mb-1 text-base font-semibold capitalize">
|
||||
<ExternalLinkIcon className="absolute top-4 right-4 size-4 text-muted-foreground" />
|
||||
<h3 className="mb-1 font-semibold text-base capitalize">
|
||||
{explorer.name}
|
||||
</h3>
|
||||
<Link
|
||||
href={explorer.url}
|
||||
target="_blank"
|
||||
className="text-sm flex gap-1.5 items-center before:absolute before:inset-0 before:z-0 text-muted-foreground"
|
||||
className="flex items-center gap-1.5 text-muted-foreground text-sm before:absolute before:inset-0 before:z-0"
|
||||
>
|
||||
{explorer.url.endsWith("/")
|
||||
? explorer.url.slice(0, -1)
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ export function PrimaryInfoItem(props: {
|
||||
return (
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h3 className="text-base text-muted-foreground font-medium">
|
||||
<h3 className="font-medium text-base text-muted-foreground">
|
||||
{props.title}
|
||||
</h3>
|
||||
{props.titleIcon}
|
||||
|
||||
@@ -79,7 +79,7 @@ export default async function ChainPageLayout({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="border-b border-border h-14 pl-7 flex">
|
||||
<div className="flex h-14 border-border border-b pl-7">
|
||||
<Breadcrumb className="my-auto">
|
||||
<BreadcrumbList>
|
||||
<BreadcrumbItem>
|
||||
@@ -119,10 +119,10 @@ export default async function ChainPageLayout({
|
||||
</BreadcrumbList>
|
||||
</Breadcrumb>
|
||||
</div>
|
||||
<div className="container flex flex-row h-full">
|
||||
<div className="container flex h-full flex-row">
|
||||
<Sidebar links={sidebarLinks} />
|
||||
|
||||
<div className="flex flex-col w-full pb-10">
|
||||
<div className="flex w-full flex-col pb-10">
|
||||
{/* Icon + Background */}
|
||||
<ChainHeader
|
||||
headerImageUrl={chainMetadata?.headerImgUrl}
|
||||
@@ -130,7 +130,7 @@ export default async function ChainPageLayout({
|
||||
chain={chain}
|
||||
/>
|
||||
|
||||
<div className="flex lg:hidden justify-end">
|
||||
<div className="flex justify-end lg:hidden">
|
||||
<MobileSidebar
|
||||
links={sidebarLinks}
|
||||
trigger={
|
||||
@@ -142,7 +142,7 @@ export default async function ChainPageLayout({
|
||||
<div className="mt-5 border-t pt-6 pb-2">
|
||||
<Link
|
||||
href="/chainlist"
|
||||
className="px-2 text-sm flex justify-start gap-2 items-center text-muted-foreground hover:text-foreground"
|
||||
className="flex items-center justify-start gap-2 px-2 text-muted-foreground text-sm hover:text-foreground"
|
||||
>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
View all chains
|
||||
@@ -157,14 +157,14 @@ export default async function ChainPageLayout({
|
||||
<div className="flex flex-col gap-3 md:gap-2">
|
||||
{/* Gas Sponsored badge - Mobile */}
|
||||
{chainMetadata?.gasSponsored && (
|
||||
<div className="md:hidden flex">
|
||||
<div className="flex md:hidden">
|
||||
<GasSponseredBadge />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Chain name */}
|
||||
<div className="flex flex-row gap-1.5 items-center">
|
||||
<h1 className="text-2xl font-semibold lg:text-3xl lg:font-semibold tracking-tight">
|
||||
<div className="flex flex-row items-center gap-1.5">
|
||||
<h1 className="font-semibold text-2xl tracking-tight lg:font-semibold lg:text-3xl">
|
||||
{chain.name}
|
||||
</h1>
|
||||
|
||||
@@ -185,13 +185,13 @@ export default async function ChainPageLayout({
|
||||
|
||||
{/* description */}
|
||||
{chainMetadata?.about && (
|
||||
<p className="text-muted-foreground text-sm lg:text-base mb-2">
|
||||
<p className="mb-2 text-muted-foreground text-sm lg:text-base">
|
||||
{chainMetadata.about}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* mobile action row */}
|
||||
<div className="sm:hidden w-full">
|
||||
<div className="w-full sm:hidden">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
<AddChainToWallet
|
||||
chain={
|
||||
@@ -219,7 +219,7 @@ export default async function ChainPageLayout({
|
||||
|
||||
function GasSponseredBadge() {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-full text-[hsl(334,81.12%,69.65%)] bg-[hsla(335,57%,51%,0.2)] px-2.5 py-1">
|
||||
<div className="flex items-center gap-2 rounded-full bg-[hsla(335,57%,51%,0.2)] px-2.5 py-1 text-[hsl(334,81.12%,69.65%)]">
|
||||
<TicketCheckIcon className="size-4" />
|
||||
<span className="font-medium text-xs">Gas Sponsored</span>
|
||||
</div>
|
||||
|
||||
@@ -34,8 +34,8 @@ export default async function Page(props: {
|
||||
|
||||
{/* Deprecated Alert */}
|
||||
{isDeprecated && (
|
||||
<section className="bg-destructive rounded-lg text-destructive-foreground border border-destructive-foreground/20">
|
||||
<div className="container py-4 flex flex-row items-center gap-4">
|
||||
<section className="rounded-lg border border-destructive-foreground/20 bg-destructive text-destructive-foreground">
|
||||
<div className="container flex flex-row items-center gap-4 py-4">
|
||||
<CircleAlertIcon className="size-6 flex-shrink-0" />
|
||||
<h3 className="font-medium">
|
||||
This chain has been marked as deprecated. Some or all services may
|
||||
@@ -63,13 +63,13 @@ export default async function Page(props: {
|
||||
{topContracts.length > 0 && (
|
||||
<section>
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="text-xl tracking-tight font-semibold">
|
||||
<h2 className="font-semibold text-xl tracking-tight">
|
||||
Popular Contracts
|
||||
</h2>
|
||||
<Button asChild variant="outline">
|
||||
<Link
|
||||
href={`/${chain.slug}/popular`}
|
||||
className="flex gap-2 items-center"
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
See All
|
||||
<ArrowRightIcon className="size-4" />
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user