Compare commits
98
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93532ecbdc | ||
|
|
f3ad6efcf8 | ||
|
|
67616c22b4 | ||
|
|
51f7418d1f | ||
|
|
dc3df4bc35 | ||
|
|
0853872d8d | ||
|
|
44e6e115d9 | ||
|
|
76cfecb305 | ||
|
|
f33d8f16e2 | ||
|
|
5c08c7c17d | ||
|
|
e20b1dd883 | ||
|
|
55a4728766 | ||
|
|
c5ef55e4f6 | ||
|
|
afd1150984 | ||
|
|
f79672ebbf | ||
|
|
3a289e80d1 | ||
|
|
e9efebaec4 | ||
|
|
d4a44f5b5f | ||
|
|
e651d0ae91 | ||
|
|
34239ea2cb | ||
|
|
8828df0a58 | ||
|
|
c6150be299 | ||
|
|
27f50a863c | ||
|
|
157f4b9a19 | ||
|
|
fbe1aa2e53 | ||
|
|
bdcbe0e25b | ||
|
|
30e217870d | ||
|
|
44fdfeef1a | ||
|
|
4cda30a87a | ||
|
|
98fab93e8b | ||
|
|
6caba380b2 | ||
|
|
68a07ed106 | ||
|
|
b075ff55a1 | ||
|
|
2d12249719 | ||
|
|
301b835b60 | ||
|
|
a02b55744b | ||
|
|
8b6deadc35 | ||
|
|
6d635dc266 | ||
|
|
1083316ef6 | ||
|
|
2c1ec47a04 | ||
|
|
e01af9db71 | ||
|
|
fd7e10d3de | ||
|
|
3126d87740 | ||
|
|
b9e63ff0a7 | ||
|
|
77053049a5 | ||
|
|
557d42d4f4 | ||
|
|
0a662c1f0c | ||
|
|
f78b0a28da | ||
|
|
e0ab3d5f27 | ||
|
|
2c7d8411b9 | ||
|
|
16ab7da64f | ||
|
|
467ba50907 | ||
|
|
2f627e1483 | ||
|
|
6b80f75e3a | ||
|
|
c43c686eac | ||
|
|
92896ddf3f | ||
|
|
a08b62be9a | ||
|
|
7f116577ab | ||
|
|
d227bc389b | ||
|
|
6c711e8783 | ||
|
|
6f581c80b3 | ||
|
|
b1ac15c2ac | ||
|
|
98eefdf90b | ||
|
|
857d4dfc16 | ||
|
|
22aad59e7d | ||
|
|
a3b2f8f4b0 | ||
|
|
d617bf77b3 | ||
|
|
4be655dd08 | ||
|
|
74d4074f42 | ||
|
|
a36c850b86 | ||
|
|
59a8dad48e | ||
|
|
f7aa3e6239 | ||
|
|
827819b911 | ||
|
|
0d7e5e563d | ||
|
|
8665e8fe14 | ||
|
|
438027a40e | ||
|
|
66c7df98e4 | ||
|
|
409ae17c15 | ||
|
|
be28b0c26e | ||
|
|
c661a485d0 | ||
|
|
90f36bc414 | ||
|
|
a77e98a48a | ||
|
|
fde39eb883 | ||
|
|
c672a84fc3 | ||
|
|
d52855dbda | ||
|
|
a3909e1794 | ||
|
|
f92201d353 | ||
|
|
45ba811950 | ||
|
|
7c458eaa49 | ||
|
|
f7ce6becd6 | ||
|
|
52d1ba32c4 | ||
|
|
1a7315c815 | ||
|
|
50c6c193c6 | ||
|
|
e114b2c4f9 | ||
|
|
d812cc370b | ||
|
|
7883127d51 | ||
|
|
53d0d74d9b | ||
|
|
c0948fb9fe |
@@ -1,14 +1,13 @@
|
||||
import type { Preview } from "@storybook/nextjs";
|
||||
import "../src/global.css";
|
||||
import "@workspace/ui/global.css";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
import { Inter as interFont } from "next/font/google";
|
||||
// biome-ignore lint/style/useImportType: <explanation>
|
||||
import React from "react";
|
||||
import { useEffect } from "react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
// biome-ignore lint/style/useImportType: ok
|
||||
import React, { useEffect } from "react";
|
||||
import { Toaster } from "sonner";
|
||||
import { Button } from "../src/@/components/ui/button";
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
@@ -69,9 +68,7 @@ const preview: Preview = {
|
||||
|
||||
export default preview;
|
||||
|
||||
function StoryLayout(props: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
function StoryLayout(props: { children: React.ReactNode }) {
|
||||
const { setTheme, theme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -21,8 +21,8 @@ module.exports = [
|
||||
"/storage",
|
||||
// -- end scale category
|
||||
|
||||
// -- nebula
|
||||
"/nebula",
|
||||
// -- ai
|
||||
"/ai",
|
||||
// -- contracts
|
||||
"/contracts",
|
||||
"/contracts/modular-contracts",
|
||||
|
||||
@@ -10,8 +10,16 @@
|
||||
"ignoreDependencies": [
|
||||
"@thirdweb-dev/service-utils",
|
||||
"@thirdweb-dev/vault-sdk",
|
||||
"thirdweb",
|
||||
"@types/color",
|
||||
"fast-xml-parser"
|
||||
"fast-xml-parser",
|
||||
"@workspace/ui",
|
||||
"tailwindcss-animate",
|
||||
"@radix-ui/react-tooltip",
|
||||
"shiki",
|
||||
"react-children-utilities",
|
||||
"react-markdown",
|
||||
"remark-gfm"
|
||||
],
|
||||
"next": true,
|
||||
"project": ["src/**"]
|
||||
|
||||
@@ -115,6 +115,7 @@ const SENTRY_OPTIONS: SentryBuildOptions = {
|
||||
const FRAMER_ADDITIONAL_LANGUAGES = ["es"];
|
||||
|
||||
const baseNextConfig: NextConfig = {
|
||||
transpilePackages: ["@workspace/ui"],
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
},
|
||||
@@ -196,7 +197,6 @@ const baseNextConfig: NextConfig = {
|
||||
]),
|
||||
];
|
||||
},
|
||||
serverExternalPackages: ["pino-pretty"],
|
||||
};
|
||||
|
||||
function getConfig(): NextConfig {
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"@radix-ui/react-slot": "^1.2.3",
|
||||
"@radix-ui/react-switch": "^1.2.5",
|
||||
"@radix-ui/react-tooltip": "1.2.7",
|
||||
"@scalar/api-reference-react": "0.7.25",
|
||||
"@sentry/nextjs": "9.34.0",
|
||||
"@shazow/whatsabi": "0.22.2",
|
||||
"@tanstack/react-query": "5.81.5",
|
||||
@@ -28,6 +27,7 @@
|
||||
"@thirdweb-dev/vault-sdk": "workspace:*",
|
||||
"@vercel/functions": "2.2.2",
|
||||
"@vercel/og": "^0.6.8",
|
||||
"@workspace/ui": "workspace:*",
|
||||
"abitype": "1.0.8",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -43,11 +43,9 @@
|
||||
"next-themes": "^0.4.6",
|
||||
"nextjs-toploader": "^1.6.12",
|
||||
"nuqs": "^2.4.3",
|
||||
"p-limit": "^6.2.0",
|
||||
"papaparse": "^5.5.3",
|
||||
"pluralize": "^8.0.0",
|
||||
"posthog-js": "1.256.1",
|
||||
"posthog-node": "^5.4.0",
|
||||
"prettier": "3.6.2",
|
||||
"react": "19.1.0",
|
||||
"react-children-utilities": "^2.10.0",
|
||||
@@ -62,7 +60,7 @@
|
||||
"remark-gfm": "4.0.1",
|
||||
"responsive-rsc": "0.0.7",
|
||||
"server-only": "^0.0.1",
|
||||
"shiki": "1.27.0",
|
||||
"shiki": "3.12.0",
|
||||
"sonner": "2.0.6",
|
||||
"spdx-correct": "^3.2.0",
|
||||
"stripe": "17.7.0",
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
autoprefixer: {},
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@workspace/ui/postcss.config";
|
||||
@@ -460,6 +460,12 @@ async function redirects() {
|
||||
permanent: false,
|
||||
source: "/universal-bridge",
|
||||
},
|
||||
// redirect /nebula to /ai
|
||||
{
|
||||
destination: "/ai",
|
||||
permanent: false,
|
||||
source: "/nebula",
|
||||
},
|
||||
...legacyDashboardToTeamRedirects,
|
||||
...projectPageRedirects,
|
||||
...teamPageRedirects,
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
import "server-only";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { PostHog } from "posthog-node";
|
||||
|
||||
let _posthogClient: PostHog | null = null;
|
||||
|
||||
function getPostHogServer(): PostHog | null {
|
||||
if (!_posthogClient && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
|
||||
_posthogClient = new PostHog(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
|
||||
host: "https://us.i.posthog.com",
|
||||
});
|
||||
}
|
||||
return _posthogClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a feature flag is enabled for a specific user
|
||||
* @param flagKey - The feature flag key
|
||||
* @param userEmail - The user's email address for filtering
|
||||
*/
|
||||
export const isFeatureFlagEnabled = unstable_cache(
|
||||
async (params: {
|
||||
flagKey: string;
|
||||
accountId: string;
|
||||
email: string | undefined;
|
||||
}): Promise<boolean> => {
|
||||
const posthogClient = getPostHogServer();
|
||||
if (!posthogClient) {
|
||||
console.warn("Posthog client not set");
|
||||
return true;
|
||||
}
|
||||
|
||||
const { flagKey, accountId, email } = params;
|
||||
|
||||
try {
|
||||
if (posthogClient && accountId) {
|
||||
const isEnabled = await posthogClient.isFeatureEnabled(
|
||||
flagKey,
|
||||
accountId,
|
||||
{
|
||||
personProperties: email ? { email } : undefined,
|
||||
},
|
||||
);
|
||||
if (isEnabled !== undefined) {
|
||||
return isEnabled;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error checking feature flag ${flagKey}:`, error);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
["is-feature-flag-enabled"],
|
||||
{
|
||||
revalidate: 3600, // 1 hour
|
||||
},
|
||||
);
|
||||
@@ -224,7 +224,11 @@ export function reportChainConfigurationAdded(properties: {
|
||||
// ASSETS
|
||||
// ----------------------------
|
||||
|
||||
type AssetContractType = "DropERC20" | "DropERC1155" | "DropERC721";
|
||||
type AssetContractType =
|
||||
| "DropERC20"
|
||||
| "DropERC1155"
|
||||
| "DropERC721"
|
||||
| "ERC20Asset";
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
@@ -334,6 +338,15 @@ export function reportAssetCreationSuccessful(properties: {
|
||||
});
|
||||
}
|
||||
|
||||
type CoinCreationStep =
|
||||
| "erc20-asset:deploy-contract"
|
||||
| "erc20-asset:airdrop-tokens"
|
||||
| "erc20-asset:approve-airdrop-tokens"
|
||||
| "drop-erc20:deploy-contract"
|
||||
| "drop-erc20:set-claim-conditions"
|
||||
| "drop-erc20:mint-tokens"
|
||||
| "drop-erc20:airdrop-tokens";
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track number of failed asset creations
|
||||
@@ -355,11 +368,7 @@ export function reportAssetCreationFailed(
|
||||
}
|
||||
| {
|
||||
assetType: "coin";
|
||||
step:
|
||||
| "deploy-contract"
|
||||
| "set-claim-conditions"
|
||||
| "mint-tokens"
|
||||
| "airdrop-tokens";
|
||||
step: CoinCreationStep;
|
||||
}
|
||||
),
|
||||
) {
|
||||
@@ -503,3 +512,38 @@ export function reportFundWalletSuccessful() {
|
||||
export function reportFundWalletFailed(params: { errorMessage: string }) {
|
||||
posthog.capture("fund wallet failed", params);
|
||||
}
|
||||
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To track the conversion rate of the users choosing to create a token from new flow instead of the old flow
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @MananTank
|
||||
*/
|
||||
export function reportTokenUpsellClicked(params: {
|
||||
assetType: "nft" | "coin";
|
||||
pageType: "explore" | "deploy-contract";
|
||||
}) {
|
||||
posthog.capture("token upsell clicked", params);
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
// CHAIN INFRASTRUCTURE CHECKOUT
|
||||
// ----------------------------
|
||||
/**
|
||||
* ### Why do we need to report this event?
|
||||
* - To record explicit user acknowledgement when proceeding to checkout without RPC
|
||||
* - To measure how often customers choose to omit RPC while purchasing Insight and/or Account Abstraction
|
||||
* - To correlate potential support issues arising from missing RPC
|
||||
*
|
||||
* ### Who is responsible for this event?
|
||||
* @jnsdls
|
||||
*/
|
||||
export function reportChainInfraRpcOmissionAgreed(properties: {
|
||||
chainId: number;
|
||||
frequency: "monthly" | "annual";
|
||||
includeInsight: boolean;
|
||||
includeAccountAbstraction: boolean;
|
||||
}) {
|
||||
posthog.capture("chain infra checkout rpc omission agreed", properties);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,6 +58,7 @@ interface WebhookSingleResponse {
|
||||
}
|
||||
|
||||
interface TestWebhookPayload {
|
||||
webhook_id?: string;
|
||||
webhook_url: string;
|
||||
type?: "event" | "transaction";
|
||||
}
|
||||
|
||||
@@ -1,362 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
|
||||
|
||||
export interface WebhookConfig {
|
||||
id: string;
|
||||
teamId: string;
|
||||
projectId: string;
|
||||
destinationUrl: string;
|
||||
description: string;
|
||||
pausedAt: string | null;
|
||||
webhookSecret: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
topics: {
|
||||
id: string;
|
||||
serviceName: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
filters: object | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
type WebhookConfigsResponse =
|
||||
| {
|
||||
data: WebhookConfig[];
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
interface CreateWebhookConfigRequest {
|
||||
topicIdsWithFilters: { id: string; filters: object | null }[];
|
||||
destinationUrl: string;
|
||||
description: string;
|
||||
isPaused?: boolean;
|
||||
}
|
||||
|
||||
type CreateWebhookConfigResponse =
|
||||
| {
|
||||
data: WebhookConfig;
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
export interface Topic {
|
||||
id: string;
|
||||
serviceName: string;
|
||||
description: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt: string | null;
|
||||
}
|
||||
|
||||
type TopicsResponse =
|
||||
| {
|
||||
data: Topic[];
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
interface UpdateWebhookConfigRequest {
|
||||
destinationUrl?: string;
|
||||
topicIdsWithFilters?: { id: string; filters: object | null }[];
|
||||
description?: string;
|
||||
isPaused?: boolean;
|
||||
}
|
||||
|
||||
type UpdateWebhookConfigResponse =
|
||||
| {
|
||||
data: WebhookConfig;
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
type DeleteWebhookConfigResponse =
|
||||
| {
|
||||
data: WebhookConfig;
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
export async function getWebhookConfigs(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
}): Promise<WebhookConfigsResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
export async function createWebhookConfig(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
config: CreateWebhookConfigRequest;
|
||||
}): Promise<CreateWebhookConfigResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs`,
|
||||
{
|
||||
body: JSON.stringify(props.config),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
export async function getAvailableTopics(): Promise<TopicsResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/webhook-topics`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "GET",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateWebhookConfig(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
webhookConfigId: string;
|
||||
config: UpdateWebhookConfigRequest;
|
||||
}): Promise<UpdateWebhookConfigResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/${props.webhookConfigId}`,
|
||||
{
|
||||
body: JSON.stringify(props.config),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "PATCH",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteWebhookConfig(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
webhookConfigId: string;
|
||||
}): Promise<DeleteWebhookConfigResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const response = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/${props.webhookConfigId}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "DELETE",
|
||||
},
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
const body = await response.text();
|
||||
return {
|
||||
body,
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
return {
|
||||
data: result.data,
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
type TestDestinationUrlResponse =
|
||||
| {
|
||||
result: {
|
||||
httpStatusCode: number;
|
||||
httpResponseBody: string;
|
||||
};
|
||||
status: "success";
|
||||
}
|
||||
| {
|
||||
body: string;
|
||||
reason: string;
|
||||
status: "error";
|
||||
};
|
||||
|
||||
export async function testDestinationUrl(props: {
|
||||
teamIdOrSlug: string;
|
||||
projectIdOrSlug: string;
|
||||
destinationUrl: string;
|
||||
}): Promise<TestDestinationUrlResponse> {
|
||||
const authToken = await getAuthToken();
|
||||
|
||||
if (!authToken) {
|
||||
return {
|
||||
body: "Authentication required",
|
||||
reason: "no_auth_token",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
|
||||
const resp = await fetch(
|
||||
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${props.teamIdOrSlug}/projects/${props.projectIdOrSlug}/webhook-configs/test-destination-url`,
|
||||
{
|
||||
body: JSON.stringify({ destinationUrl: props.destinationUrl }),
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
method: "POST",
|
||||
},
|
||||
);
|
||||
if (!resp.ok) {
|
||||
return {
|
||||
body: await resp.text(),
|
||||
reason: "unknown",
|
||||
status: "error",
|
||||
};
|
||||
}
|
||||
return await resp.json();
|
||||
}
|
||||
@@ -12,6 +12,7 @@ export type AuthOption =
|
||||
| "google"
|
||||
| "facebook"
|
||||
| "x"
|
||||
| "tiktok"
|
||||
| "discord"
|
||||
| "farcaster"
|
||||
| "telegram"
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
|
||||
import { UB_BASE_URL } from "./constants";
|
||||
import type { TokenMetadata } from "./types";
|
||||
|
||||
export async function getUniversalBridgeTokens(props: {
|
||||
chainId?: number;
|
||||
address?: string;
|
||||
}) {
|
||||
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
|
||||
|
||||
if (props.chainId) {
|
||||
url.searchParams.append("chainId", String(props.chainId));
|
||||
}
|
||||
if (props.address) {
|
||||
url.searchParams.append("tokenAddress", props.address);
|
||||
}
|
||||
url.searchParams.append("limit", "1000");
|
||||
url.searchParams.append("includePrices", "false");
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-client-id": NEXT_PUBLIC_DASHBOARD_CLIENT_ID,
|
||||
} as Record<string, string>,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Array<TokenMetadata>;
|
||||
}
|
||||
@@ -1,48 +1,8 @@
|
||||
"use server";
|
||||
import type { ProjectResponse } from "@thirdweb-dev/service-utils";
|
||||
import { getAuthToken } from "@/api/auth-token";
|
||||
import { DASHBOARD_THIRDWEB_SECRET_KEY } from "@/constants/server-envs";
|
||||
import { UB_BASE_URL } from "./constants";
|
||||
|
||||
export type TokenMetadata = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
address: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
iconUri?: string;
|
||||
};
|
||||
|
||||
export async function getUniversalBridgeTokens(props: {
|
||||
chainId?: number;
|
||||
address?: string;
|
||||
}) {
|
||||
const url = new URL(`${UB_BASE_URL}/v1/tokens`);
|
||||
|
||||
if (props.chainId) {
|
||||
url.searchParams.append("chainId", String(props.chainId));
|
||||
}
|
||||
if (props.address) {
|
||||
url.searchParams.append("tokenAddress", props.address);
|
||||
}
|
||||
url.searchParams.append("limit", "1000");
|
||||
|
||||
const res = await fetch(url.toString(), {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-secret-key": DASHBOARD_THIRDWEB_SECRET_KEY,
|
||||
} as Record<string, string>,
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const text = await res.text();
|
||||
throw new Error(text);
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
return json.data as Array<TokenMetadata>;
|
||||
}
|
||||
import type { TokenMetadata } from "./types";
|
||||
|
||||
export async function addUniversalBridgeTokenRoute(props: {
|
||||
chainId: number;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
export type TokenMetadata = {
|
||||
name: string;
|
||||
symbol: string;
|
||||
address: string;
|
||||
decimals: number;
|
||||
chainId: number;
|
||||
iconUri?: string;
|
||||
};
|
||||
@@ -86,7 +86,9 @@ export function getLastNDaysRange(id: DurationId) {
|
||||
throw new Error(`Invalid duration id: ${id}`);
|
||||
}
|
||||
|
||||
const todayDate = new Date(Date.now());
|
||||
const todayDate = new Date();
|
||||
todayDate.setDate(todayDate.getDate() + 1); // Move to next day
|
||||
todayDate.setHours(0, 0, 0, 0); // Set to start of next day (00:00)
|
||||
|
||||
const value: Range = {
|
||||
from: subDays(todayDate, durationInfo.days),
|
||||
|
||||
@@ -22,7 +22,7 @@ export function ResponsiveTimeFilters(props: { defaultRange: DurationId }) {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex justify-end gap-3 flex-col lg:flex-row">
|
||||
<div className="flex gap-3 flex-row">
|
||||
<DateRangeSelector
|
||||
className="rounded-full"
|
||||
range={range}
|
||||
|
||||
@@ -7,7 +7,7 @@ import { toast } from "sonner";
|
||||
import { reSubscribePlan } from "@/actions/billing";
|
||||
import type { Team } from "@/api/team/get-team";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { pollWithTimeout } from "@/utils/pollWithTimeout";
|
||||
import { tryCatch } from "@/utils/try-catch";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "../../lib/utils";
|
||||
import { DynamicHeight } from "../ui/DynamicHeight";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DownloadIcon } from "lucide-react";
|
||||
import Papa from "papaparse";
|
||||
import { toast } from "sonner";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
export function ExportToCSVButton(props: {
|
||||
|
||||
@@ -1,87 +1,3 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
"use client";
|
||||
import { useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useIsomorphicLayoutEffect } from "../../lib/useIsomorphicLayoutEffect";
|
||||
|
||||
type imgElementProps = React.DetailedHTMLProps<
|
||||
React.ImgHTMLAttributes<HTMLImageElement>,
|
||||
HTMLImageElement
|
||||
> & {
|
||||
skeleton?: React.ReactNode;
|
||||
fallback?: React.ReactNode;
|
||||
src: string | undefined;
|
||||
containerClassName?: string;
|
||||
};
|
||||
|
||||
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, containerClassName, ...restProps } =
|
||||
props;
|
||||
const defaultSkeleton = <div className="animate-pulse bg-accent" />;
|
||||
const defaultFallback = <div className="bg-accent" />;
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const imgEl = imgRef.current;
|
||||
if (!imgEl) {
|
||||
return;
|
||||
}
|
||||
if (imgEl.complete) {
|
||||
setStatus("loaded");
|
||||
} else {
|
||||
function handleLoad() {
|
||||
setStatus("loaded");
|
||||
}
|
||||
imgEl.addEventListener("load", handleLoad);
|
||||
return () => {
|
||||
imgEl.removeEventListener("load", handleLoad);
|
||||
};
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={cn("relative shrink-0", containerClassName)}>
|
||||
<img
|
||||
{...restProps}
|
||||
// avoid setting empty src string to prevent request to the entire page
|
||||
alt={restProps.alt || ""}
|
||||
className={cn(
|
||||
"fade-in-0 object-cover transition-opacity duration-300",
|
||||
className,
|
||||
)}
|
||||
decoding="async"
|
||||
onError={() => {
|
||||
setStatus("fallback");
|
||||
}}
|
||||
ref={imgRef}
|
||||
src={restProps.src || undefined}
|
||||
style={{
|
||||
opacity: status === "loaded" ? 1 : 0,
|
||||
...restProps.style,
|
||||
}}
|
||||
/>
|
||||
|
||||
{status !== "loaded" && (
|
||||
<div
|
||||
className={cn(
|
||||
"fade-in-0 absolute inset-0 overflow-hidden transition-opacity duration-300 [&>*]:h-full [&>*]:w-full",
|
||||
className,
|
||||
)}
|
||||
style={restProps.style}
|
||||
>
|
||||
{status === "pending" && (skeleton || defaultSkeleton)}
|
||||
{status === "fallback" && (fallback || defaultFallback)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
export { Img } from "@workspace/ui/components/img";
|
||||
|
||||
@@ -29,16 +29,18 @@ export function MultiNetworkSelector(props: {
|
||||
client: ThirdwebClient;
|
||||
chainIds?: number[];
|
||||
}) {
|
||||
let { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
if (props.chainIds && props.chainIds.length > 0) {
|
||||
allChains = allChains.filter((chain) =>
|
||||
props.chainIds?.includes(chain.chainId),
|
||||
);
|
||||
}
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
const options = useMemo(() => {
|
||||
let sortedChains = allChains;
|
||||
let chains = allChains.filter((chain) => chain.status !== "deprecated");
|
||||
|
||||
if (props.chainIds && props.chainIds.length > 0) {
|
||||
chains = allChains.filter((chain) =>
|
||||
props.chainIds?.includes(chain.chainId),
|
||||
);
|
||||
}
|
||||
|
||||
let sortedChains = chains;
|
||||
|
||||
if (props.priorityChains) {
|
||||
const priorityChainsSet = new Set();
|
||||
@@ -69,7 +71,13 @@ export function MultiNetworkSelector(props: {
|
||||
value: String(chain.chainId),
|
||||
};
|
||||
});
|
||||
}, [allChains, props.priorityChains, idToChain, props.hideTestnets]);
|
||||
}, [
|
||||
allChains,
|
||||
props.priorityChains,
|
||||
idToChain,
|
||||
props.hideTestnets,
|
||||
props.chainIds,
|
||||
]);
|
||||
|
||||
const searchFn = useCallback(
|
||||
(option: Option, searchValue: string) => {
|
||||
@@ -155,16 +163,38 @@ export function SingleNetworkSelector(props: {
|
||||
disableDeprecated?: boolean;
|
||||
placeholder?: string;
|
||||
client: ThirdwebClient;
|
||||
priorityChains?: number[];
|
||||
}) {
|
||||
const { allChains, idToChain } = useAllChainsData();
|
||||
|
||||
const chainsToShow = useMemo(() => {
|
||||
let chains = allChains;
|
||||
|
||||
chains = chains.filter((chain) => chain.status !== "deprecated");
|
||||
|
||||
if (props.disableTestnets) {
|
||||
chains = chains.filter((chain) => !chain.testnet);
|
||||
}
|
||||
|
||||
if (props.priorityChains) {
|
||||
const priorityChainsSet = new Set();
|
||||
for (const chainId of props.priorityChains || []) {
|
||||
priorityChainsSet.add(chainId);
|
||||
}
|
||||
|
||||
const priorityChains = (props.priorityChains || [])
|
||||
.map((chainId) => {
|
||||
return idToChain.get(chainId);
|
||||
})
|
||||
.filter((v) => !!v);
|
||||
|
||||
const otherChains = allChains.filter(
|
||||
(chain) => !priorityChainsSet.has(chain.chainId),
|
||||
);
|
||||
|
||||
chains = [...priorityChains, ...otherChains];
|
||||
}
|
||||
|
||||
if (props.chainIds) {
|
||||
const chainIdSet = new Set(props.chainIds);
|
||||
chains = chains.filter((chain) => chainIdSet.has(chain.chainId));
|
||||
@@ -180,6 +210,8 @@ export function SingleNetworkSelector(props: {
|
||||
props.chainIds,
|
||||
props.disableTestnets,
|
||||
props.disableDeprecated,
|
||||
props.priorityChains,
|
||||
idToChain,
|
||||
]);
|
||||
|
||||
const options = useMemo(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function RouteDiscoveryCard(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type React from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function SettingsCard(
|
||||
|
||||
@@ -10,13 +10,14 @@ interface SignatureOption {
|
||||
|
||||
interface SignatureSelectorProps {
|
||||
options: SignatureOption[];
|
||||
value: string;
|
||||
onChange: (val: string) => void;
|
||||
value: string | string[];
|
||||
onChange: (val: string | string[]) => void;
|
||||
setAbi?: (abi: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
secondaryTextFormatter?: (sig: SignatureOption) => string;
|
||||
className?: string;
|
||||
multiSelect?: boolean;
|
||||
}
|
||||
|
||||
export function SignatureSelector({
|
||||
@@ -28,6 +29,7 @@ export function SignatureSelector({
|
||||
disabled,
|
||||
secondaryTextFormatter,
|
||||
className,
|
||||
multiSelect = false,
|
||||
}: SignatureSelectorProps) {
|
||||
const [searchValue, setSearchValue] = useState("");
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@@ -42,38 +44,96 @@ export function SignatureSelector({
|
||||
}));
|
||||
}, [options, secondaryTextFormatter]);
|
||||
|
||||
// Check if the current value is a custom value (not in options)
|
||||
const isCustomValue = value && !options.some((opt) => opt.value === value);
|
||||
|
||||
// Add the custom value as an option if needed
|
||||
const allOptions = useMemo(() => {
|
||||
if (isCustomValue && value) {
|
||||
return [...formattedOptions, { label: value, value }];
|
||||
// Handle both single and multi-select values
|
||||
const currentValues = useMemo((): string[] => {
|
||||
if (multiSelect) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.filter(
|
||||
(val): val is string =>
|
||||
val !== undefined && val !== null && val !== "",
|
||||
);
|
||||
} else {
|
||||
return value ? [value] : [];
|
||||
}
|
||||
} else {
|
||||
if (Array.isArray(value)) {
|
||||
return value.length > 0 && value[0] ? [value[0]] : [];
|
||||
} else {
|
||||
return value ? [value] : [];
|
||||
}
|
||||
}
|
||||
return formattedOptions;
|
||||
}, [formattedOptions, isCustomValue, value]);
|
||||
}, [value, multiSelect]);
|
||||
|
||||
// Single-select MultiSelect wrapper
|
||||
// Check if the current values include custom values (not in options)
|
||||
const customValues = useMemo((): string[] => {
|
||||
return currentValues.filter(
|
||||
(val): val is string =>
|
||||
val !== undefined &&
|
||||
val !== null &&
|
||||
val !== "" &&
|
||||
!options.some((opt) => opt.value === val),
|
||||
);
|
||||
}, [currentValues, options]);
|
||||
|
||||
// Add the custom values as options if needed
|
||||
const allOptions = useMemo(() => {
|
||||
const customOptions = customValues.map((val) => ({
|
||||
label: val,
|
||||
value: val,
|
||||
}));
|
||||
return [...formattedOptions, ...customOptions];
|
||||
}, [formattedOptions, customValues]);
|
||||
|
||||
// Multi-select or single-select MultiSelect wrapper
|
||||
const handleSelectedValuesChange = useCallback(
|
||||
(selected: string[]) => {
|
||||
// Always use the last selected value for single-select behavior
|
||||
const selectedValue =
|
||||
selected.length > 0 ? (selected[selected.length - 1] ?? "") : "";
|
||||
onChange(selectedValue);
|
||||
const found = options.find((opt) => opt.value === selectedValue);
|
||||
if (setAbi) {
|
||||
setAbi(found?.abi || "");
|
||||
if (multiSelect) {
|
||||
// Multi-select behavior
|
||||
onChange(selected);
|
||||
// For multi-select, we'll use the ABI from the first selected option that has one
|
||||
const firstOptionWithAbi = selected.find((selectedValue) => {
|
||||
const found = options.find((opt) => opt.value === selectedValue);
|
||||
return found?.abi;
|
||||
});
|
||||
if (setAbi && firstOptionWithAbi) {
|
||||
const found = options.find((opt) => opt.value === firstOptionWithAbi);
|
||||
setAbi(found?.abi || "");
|
||||
}
|
||||
} else {
|
||||
// Single-select behavior (maintain backward compatibility)
|
||||
const selectedValue =
|
||||
selected.length > 0 ? (selected[selected.length - 1] ?? "") : "";
|
||||
onChange(selectedValue);
|
||||
const found = options.find((opt) => opt.value === selectedValue);
|
||||
if (setAbi) {
|
||||
setAbi(found?.abi || "");
|
||||
}
|
||||
}
|
||||
setSearchValue("");
|
||||
},
|
||||
[onChange, setAbi, options],
|
||||
[onChange, setAbi, options, multiSelect],
|
||||
);
|
||||
|
||||
// Handle custom value entry
|
||||
const handleInputKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === "Enter" && searchValue.trim()) {
|
||||
if (!options.some((opt) => opt.value === searchValue.trim())) {
|
||||
onChange(searchValue.trim());
|
||||
if (multiSelect) {
|
||||
// Add to existing values for multi-select
|
||||
const currentArray = Array.isArray(value)
|
||||
? value
|
||||
: value
|
||||
? [value]
|
||||
: [];
|
||||
const filteredArray = currentArray.filter(
|
||||
(val): val is string => val !== undefined && val !== null,
|
||||
);
|
||||
const newValues = [...filteredArray, searchValue.trim()];
|
||||
onChange(newValues);
|
||||
} else {
|
||||
// Replace value for single-select
|
||||
onChange(searchValue.trim());
|
||||
}
|
||||
if (setAbi) setAbi("");
|
||||
setSearchValue("");
|
||||
// Optionally blur input
|
||||
@@ -106,7 +166,7 @@ export function SignatureSelector({
|
||||
customSearchInput={customSearchInput}
|
||||
customTrigger={null}
|
||||
disabled={disabled}
|
||||
maxCount={1}
|
||||
maxCount={multiSelect ? 100 : 1}
|
||||
onSelectedValuesChange={handleSelectedValuesChange}
|
||||
options={allOptions}
|
||||
overrideSearchFn={(option, searchTerm) =>
|
||||
@@ -116,11 +176,13 @@ export function SignatureSelector({
|
||||
placeholder={placeholder}
|
||||
renderOption={(option) => <span>{option.label}</span>}
|
||||
searchPlaceholder={placeholder}
|
||||
selectedValues={value ? [value] : []}
|
||||
selectedValues={currentValues}
|
||||
/>
|
||||
{isCustomValue && (
|
||||
{customValues.length > 0 && (
|
||||
<div className="mt-2 rounded border border-warning-200 bg-warning-50 px-2 py-1 text-warning-700 text-xs">
|
||||
You entered a custom signature. Please provide the ABI below.
|
||||
{multiSelect
|
||||
? `You entered ${customValues.length} custom signature${customValues.length > 1 ? "s" : ""}. Please provide the ABI below.`
|
||||
: "You entered a custom signature. Please provide the ABI below."}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import {
|
||||
Table,
|
||||
|
||||
@@ -5,16 +5,16 @@ import {
|
||||
type ThirdwebClient,
|
||||
} from "thirdweb";
|
||||
import { shortenAddress } from "thirdweb/utils";
|
||||
import type { TokenMetadata } from "@/api/universal-bridge/tokens";
|
||||
import type { TokenMetadata } from "@/api/universal-bridge/types";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { SelectWithSearch } from "@/components/blocks/select-with-search";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { fallbackChainIcon } from "@/constants/chain";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useTokensData } from "@/hooks/tokens";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { Spinner } from "../ui/Spinner/Spinner";
|
||||
|
||||
type Option = { label: string; value: string };
|
||||
|
||||
@@ -204,7 +204,7 @@ export function TokenSelector(props: {
|
||||
searchPlaceholder="Search by name or symbol"
|
||||
showCheck={props.showCheck}
|
||||
side={props.side}
|
||||
value={selectedValue}
|
||||
value={tokensQuery.isPending ? undefined : selectedValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,12 +1,39 @@
|
||||
"use client";
|
||||
|
||||
import { XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useLocalStorage } from "@/hooks/useLocalStorage";
|
||||
|
||||
export function DismissibleAlert(props: {
|
||||
export function DismissibleAlert(
|
||||
props: {
|
||||
title: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
className?: string;
|
||||
description: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
} & (
|
||||
| {
|
||||
preserveState: true;
|
||||
localStorageId: string;
|
||||
}
|
||||
| {
|
||||
preserveState: false;
|
||||
}
|
||||
),
|
||||
) {
|
||||
if (props.preserveState) {
|
||||
return <DismissibleAlertWithLocalStorage {...props} />;
|
||||
}
|
||||
|
||||
return <DismissibleAlertWithoutLocalStorage {...props} />;
|
||||
}
|
||||
|
||||
function DismissibleAlertWithLocalStorage(props: {
|
||||
title: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
localStorageId: string;
|
||||
}) {
|
||||
const [isVisible, setIsVisible] = useLocalStorage(
|
||||
@@ -17,19 +44,48 @@ export function DismissibleAlert(props: {
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return <AlertUI {...props} onClose={() => setIsVisible(false)} />;
|
||||
}
|
||||
|
||||
function DismissibleAlertWithoutLocalStorage(props: {
|
||||
title: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
}) {
|
||||
const [isVisible, setIsVisible] = useState(true);
|
||||
|
||||
if (!isVisible) return null;
|
||||
|
||||
return <AlertUI {...props} onClose={() => setIsVisible(false)} />;
|
||||
}
|
||||
|
||||
function AlertUI(props: {
|
||||
title: React.ReactNode;
|
||||
header?: React.ReactNode;
|
||||
description: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="relative rounded-lg border border-border bg-card p-4">
|
||||
<Button
|
||||
aria-label="Close alert"
|
||||
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
|
||||
onClick={() => setIsVisible(false)}
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
<h2 className="mb-0.5 font-semibold">{props.title} </h2>
|
||||
<div className="text-muted-foreground text-sm">{props.description}</div>
|
||||
<div className={props.className}>
|
||||
<div className="relative rounded-lg border border-border bg-card p-4 lg:p-6 overflow-hidden">
|
||||
<Button
|
||||
aria-label="Close alert"
|
||||
className="absolute top-4 right-4 h-auto w-auto p-1 text-muted-foreground"
|
||||
onClick={props.onClose}
|
||||
variant="ghost"
|
||||
>
|
||||
<XIcon className="size-5" />
|
||||
</Button>
|
||||
<div>
|
||||
{props.header}
|
||||
<h2 className="mb-0.5 font-semibold">{props.title} </h2>
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{props.description}
|
||||
</div>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -3,12 +3,15 @@ import { cn } from "@/lib/utils";
|
||||
export type Segment = {
|
||||
label: string;
|
||||
percent: number;
|
||||
value: string;
|
||||
color: string;
|
||||
};
|
||||
|
||||
type DistributionBarChartProps = {
|
||||
segments: Segment[];
|
||||
title: string;
|
||||
title?: string;
|
||||
titleClassName?: string;
|
||||
barClassName?: string;
|
||||
};
|
||||
|
||||
export function DistributionBarChart(props: DistributionBarChartProps) {
|
||||
@@ -21,24 +24,36 @@ export function DistributionBarChart(props: DistributionBarChartProps) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className="font-medium text-sm">{props.title}</h3>
|
||||
<div
|
||||
className={cn(
|
||||
"font-medium text-muted-foreground text-sm",
|
||||
invalidTotalPercentage && "text-red-500",
|
||||
)}
|
||||
>
|
||||
Total: {totalPercentage}%
|
||||
{props.title && (
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<h3 className={cn("font-medium text-sm", props.titleClassName)}>
|
||||
{props.title}
|
||||
</h3>
|
||||
<div
|
||||
className={cn(
|
||||
"font-medium text-muted-foreground text-sm",
|
||||
invalidTotalPercentage && "text-red-500",
|
||||
)}
|
||||
>
|
||||
Total: {totalPercentage}%
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bar */}
|
||||
<div className="flex h-3 overflow-hidden rounded-lg">
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-3 overflow-hidden rounded-lg",
|
||||
props.barClassName,
|
||||
)}
|
||||
>
|
||||
{props.segments.map((segment) => {
|
||||
return (
|
||||
<div
|
||||
className="flex h-full items-center justify-center transition-all duration-200"
|
||||
className={cn(
|
||||
"flex h-full items-center justify-center transition-all duration-200",
|
||||
segment.percent > 0 && "border-r-2 border-background",
|
||||
)}
|
||||
key={segment.label}
|
||||
style={{
|
||||
backgroundColor: segment.color,
|
||||
@@ -67,7 +82,7 @@ export function DistributionBarChart(props: DistributionBarChartProps) {
|
||||
"text-destructive-text",
|
||||
)}
|
||||
>
|
||||
{segment.label}: {segment.percent}%
|
||||
{segment.label}: {segment.value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { useMemo } from "react";
|
||||
import { CopyTextButton } from "@/components/ui/CopyTextButton";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
|
||||
export function UnexpectedValueErrorMessage(props: {
|
||||
value: unknown;
|
||||
|
||||
@@ -91,7 +91,7 @@ export function FullWidthSidebarLayout(props: {
|
||||
<main className="flex min-w-0 grow flex-col max-sm:w-full">
|
||||
{children}
|
||||
</main>
|
||||
<AppFooter containerClassName="max-w-7xl" />
|
||||
<AppFooter />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,225 +1 @@
|
||||
import { onlyText } from "react-children-utilities";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code";
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export const MarkdownRenderer: React.FC<{
|
||||
markdownText: string;
|
||||
className?: string;
|
||||
code?: {
|
||||
disableCodeHighlight?: boolean;
|
||||
ignoreFormattingErrors?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
inlineCode?: {
|
||||
className?: string;
|
||||
};
|
||||
p?: {
|
||||
className?: string;
|
||||
};
|
||||
li?: {
|
||||
className?: string;
|
||||
};
|
||||
skipHtml?: boolean;
|
||||
}> = (markdownProps) => {
|
||||
const { markdownText, className, code } = markdownProps;
|
||||
const commonHeadingClassName = "mb-2 leading-5 font-semibold tracking-tight";
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
a: (props) => (
|
||||
<UnderlineLink
|
||||
href={props.href ?? "#"}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
{...cleanedProps(props)}
|
||||
className="mt-4"
|
||||
/>
|
||||
),
|
||||
|
||||
code: ({ ...props }) => {
|
||||
const codeStr = onlyText(props.children);
|
||||
|
||||
if (props?.className || codeStr.length > 100) {
|
||||
if (code?.disableCodeHighlight || !props.className) {
|
||||
return (
|
||||
<div className="my-4">
|
||||
{/* @ts-expect-error - TODO: fix this */}
|
||||
<PlainTextCodeBlock
|
||||
{...cleanedProps(props)}
|
||||
className={markdownProps.code?.className}
|
||||
code={onlyText(props.children).trim()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const language = props.className.replace("language-", "");
|
||||
return (
|
||||
<div className="my-4">
|
||||
<CodeClient
|
||||
// @ts-expect-error - TODO: fix this
|
||||
lang={language}
|
||||
{...cleanedProps(props)}
|
||||
className={markdownProps.code?.className}
|
||||
code={onlyText(props.children).trim()}
|
||||
ignoreFormattingErrors={code?.ignoreFormattingErrors}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InlineCode
|
||||
className={markdownProps.inlineCode?.className}
|
||||
code={onlyText(props.children).trim()}
|
||||
/>
|
||||
);
|
||||
},
|
||||
h1: (props) => (
|
||||
<h2
|
||||
className={cn(
|
||||
commonHeadingClassName,
|
||||
"mb-3 border-dashed border-b pb-2 text-xl md:text-2xl",
|
||||
)}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
|
||||
h2: (props) => (
|
||||
<h3
|
||||
{...cleanedProps(props)}
|
||||
className={cn(
|
||||
commonHeadingClassName,
|
||||
"mt-8 mb-3 border-dashed border-b pb-2 text-lg md:text-xl",
|
||||
)}
|
||||
/>
|
||||
),
|
||||
|
||||
h3: (props) => (
|
||||
<h4
|
||||
{...cleanedProps(props)}
|
||||
className={cn(
|
||||
commonHeadingClassName,
|
||||
"mt-4 text-base md:text-lg",
|
||||
)}
|
||||
/>
|
||||
),
|
||||
|
||||
h4: (props) => (
|
||||
<h5
|
||||
{...cleanedProps(props)}
|
||||
className={cn(commonHeadingClassName, "mt-4 text-lg")}
|
||||
/>
|
||||
),
|
||||
|
||||
h5: (props) => (
|
||||
<h6
|
||||
{...cleanedProps(props)}
|
||||
className={cn(commonHeadingClassName, "mt-4 text-lg")}
|
||||
/>
|
||||
),
|
||||
|
||||
h6: (props) => (
|
||||
<p
|
||||
{...cleanedProps(props)}
|
||||
className={cn(commonHeadingClassName, "mt-4 text-lg")}
|
||||
/>
|
||||
),
|
||||
|
||||
hr: (props) => (
|
||||
<hr {...cleanedProps(props)} className="my-5 bg-border" />
|
||||
),
|
||||
li: ({ children: c, ...props }) => (
|
||||
<li
|
||||
className={cn(
|
||||
"text-muted-foreground leading-relaxed [&>p]:m-0",
|
||||
markdownProps.li?.className,
|
||||
)}
|
||||
{...cleanedProps(props)}
|
||||
>
|
||||
{c}
|
||||
</li>
|
||||
),
|
||||
ol: (props) => (
|
||||
<ol
|
||||
className="mb-4 list-outside list-decimal pl-4 space-y-2 [&>li]:first:mt-2"
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
|
||||
p: (props) => (
|
||||
<p
|
||||
className={cn(
|
||||
"mb-3 text-muted-foreground leading-7",
|
||||
markdownProps.p?.className,
|
||||
)}
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
),
|
||||
strong(props) {
|
||||
return <strong className="font-medium" {...cleanedProps(props)} />;
|
||||
},
|
||||
|
||||
table: (props) => (
|
||||
<div className="mb-6">
|
||||
<TableContainer>
|
||||
<Table {...cleanedProps(props)} />
|
||||
</TableContainer>
|
||||
</div>
|
||||
),
|
||||
tbody: (props) => <TableBody {...cleanedProps(props)} />,
|
||||
|
||||
td: (props) => (
|
||||
<TableCell {...cleanedProps(props)} className="text-left" />
|
||||
),
|
||||
|
||||
th: ({ children: c, ...props }) => (
|
||||
<TableHead
|
||||
{...cleanedProps(props)}
|
||||
className="text-left text-muted-foreground"
|
||||
>
|
||||
{c}
|
||||
</TableHead>
|
||||
),
|
||||
thead: (props) => <TableHeader {...cleanedProps(props)} />,
|
||||
tr: (props) => <TableRow {...cleanedProps(props)} />,
|
||||
ul: (props) => {
|
||||
return (
|
||||
<ul
|
||||
className="mb-4 list-outside list-disc pl-4 space-y-2 [&>li]:first:mt-2"
|
||||
{...cleanedProps(props)}
|
||||
/>
|
||||
);
|
||||
},
|
||||
}}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
skipHtml={markdownProps.skipHtml}
|
||||
>
|
||||
{markdownText}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function cleanedProps<T extends object & { node?: unknown }>(
|
||||
props: T,
|
||||
): Omit<T, "node"> {
|
||||
// biome-ignore lint/correctness/noUnusedVariables: EXPECTED
|
||||
const { node, ...rest } = props;
|
||||
return rest;
|
||||
}
|
||||
export { MarkdownRenderer } from "@workspace/ui/components/markdown-renderer";
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useShowMore } from "@/hooks/useShowMore";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -64,6 +64,7 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
popoverContentClassName,
|
||||
showSelectedValuesInModal = false,
|
||||
customSearchInput,
|
||||
customTrigger,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
@@ -144,13 +145,18 @@ export const MultiSelect = forwardRef<HTMLButtonElement, MultiSelectProps>(
|
||||
// scroll to top when options change
|
||||
const popoverElRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Filter out customTrigger from props to avoid passing it to Button
|
||||
const buttonProps = Object.fromEntries(
|
||||
Object.entries(props).filter(([key]) => key !== "customTrigger"),
|
||||
) as React.ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
return (
|
||||
<Popover modal onOpenChange={setIsPopoverOpen} open={isPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
{props.customTrigger || (
|
||||
{customTrigger || (
|
||||
<Button
|
||||
ref={ref}
|
||||
{...props}
|
||||
{...buttonProps}
|
||||
className={cn(
|
||||
"flex h-auto min-h-10 w-full items-center justify-between rounded-md border border-border bg-inherit p-3 hover:bg-inherit",
|
||||
className,
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
RefreshCwIcon,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "../../ui/DynamicHeight";
|
||||
import { Spinner } from "../../ui/Spinner/Spinner";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
|
||||
export type MultiStepState<T extends string> = {
|
||||
id: T;
|
||||
@@ -34,7 +34,7 @@ export function MultiStepStatus<T extends string>(props: {
|
||||
}) {
|
||||
return (
|
||||
<DynamicHeight>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 overflow-hidden">
|
||||
{props.steps.map((step) => (
|
||||
<div className="flex items-start space-x-3 " key={step.label}>
|
||||
{step.status.type === "completed" ? (
|
||||
@@ -46,7 +46,7 @@ export function MultiStepStatus<T extends string>(props: {
|
||||
) : (
|
||||
<CircleIcon className="mt-0.5 size-5 flex-shrink-0 text-muted-foreground/70" />
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<div className="grow">
|
||||
<p
|
||||
className={`font-medium ${
|
||||
step.status.type === "pending"
|
||||
@@ -73,7 +73,7 @@ export function MultiStepStatus<T extends string>(props: {
|
||||
{step.status.type === "error"
|
||||
? props.renderError?.(step, step.status.message) || (
|
||||
<div className="mt-1 space-y-2">
|
||||
<p className="mb-1 text-red-500 text-sm">
|
||||
<p className="mb-1 text-red-500 text-sm whitespace-pre-wrap break-all">
|
||||
{step.status.message}
|
||||
</p>
|
||||
<Button
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import {
|
||||
BookTextIcon,
|
||||
BoxIcon,
|
||||
ChevronDownIcon,
|
||||
CodeIcon,
|
||||
WebhookIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMemo } from "react";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
|
||||
type LinkType = "api" | "docs" | "playground" | "webhooks";
|
||||
|
||||
const linkTypeToLabel: Record<LinkType, string> = {
|
||||
api: "API Reference",
|
||||
docs: "Documentation",
|
||||
playground: "Playground",
|
||||
webhooks: "Webhooks",
|
||||
};
|
||||
|
||||
const linkTypeToOrder: Record<LinkType, number> = {
|
||||
docs: 0,
|
||||
playground: 1,
|
||||
api: 3,
|
||||
webhooks: 4,
|
||||
};
|
||||
|
||||
const linkTypeToIcon: Record<LinkType, React.FC<{ className?: string }>> = {
|
||||
api: CodeIcon,
|
||||
docs: BookTextIcon,
|
||||
playground: BoxIcon,
|
||||
webhooks: WebhookIcon,
|
||||
};
|
||||
|
||||
function orderLinks(links: ActionLink[]) {
|
||||
return links.slice().sort((a, b) => {
|
||||
const aIndex = linkTypeToOrder[a.type];
|
||||
const bIndex = linkTypeToOrder[b.type];
|
||||
return aIndex - bIndex;
|
||||
});
|
||||
}
|
||||
|
||||
export type ActionLink = {
|
||||
type: LinkType;
|
||||
href: string;
|
||||
};
|
||||
|
||||
export function LinkGroup(props: { links: ActionLink[] }) {
|
||||
const orderedLinks = useMemo(() => orderLinks(props.links), [props.links]);
|
||||
|
||||
if (orderedLinks.length === 1 && orderedLinks[0]) {
|
||||
const link = orderedLinks[0];
|
||||
const Icon = linkTypeToIcon[link.type];
|
||||
return (
|
||||
<Link
|
||||
href={link.href}
|
||||
target={link.href.startsWith("http") ? "_blank" : undefined}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full border gap-2 bg-card"
|
||||
>
|
||||
<Icon className="size-3.5 text-muted-foreground" />
|
||||
{linkTypeToLabel[link.type]}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full border gap-2 bg-card [&[data-state=open]>svg]:rotate-180"
|
||||
>
|
||||
Resources
|
||||
<ChevronDownIcon className="size-4 transition-transform duration-200 text-muted-foreground" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="gap-1 flex flex-col w-48 rounded-xl"
|
||||
sideOffset={10}
|
||||
>
|
||||
{orderedLinks.map((link) => {
|
||||
const isExternal = link.href.startsWith("http");
|
||||
const Icon = linkTypeToIcon[link.type];
|
||||
return (
|
||||
<DropdownMenuItem
|
||||
key={link.type}
|
||||
asChild
|
||||
className="flex flex-row items-center gap-2 cursor-pointer py-1.5"
|
||||
>
|
||||
<Link
|
||||
href={link.href}
|
||||
target={isExternal ? "_blank" : undefined}
|
||||
rel={isExternal ? "noopener noreferrer" : undefined}
|
||||
>
|
||||
<Icon className="size-4 text-muted-foreground" />
|
||||
{linkTypeToLabel[link.type]}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import {
|
||||
type FooterCardProps,
|
||||
FooterLinksSection,
|
||||
} from "../../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/components/footer/FooterLinksSection";
|
||||
|
||||
export type ProjectPageFooterProps = FooterCardProps;
|
||||
|
||||
export function ProjectPageFooter(props: ProjectPageFooterProps) {
|
||||
return (
|
||||
<footer className="container max-w-7xl">
|
||||
<FooterLinksSection {...props} />
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import { ArrowUpRightIcon, Settings2Icon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type ActionLink, LinkGroup } from "./header/link-group";
|
||||
|
||||
type Action =
|
||||
| ({
|
||||
label: string;
|
||||
href: string;
|
||||
} & (
|
||||
| {
|
||||
external: true;
|
||||
icon?: never;
|
||||
}
|
||||
| {
|
||||
icon?: React.ReactNode;
|
||||
external?: false;
|
||||
}
|
||||
))
|
||||
| {
|
||||
component: React.ReactNode;
|
||||
};
|
||||
|
||||
function Action(props: { action: Action; variant?: "default" | "secondary" }) {
|
||||
const action = props.action;
|
||||
|
||||
return "component" in action ? (
|
||||
action.component
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
className={cn(
|
||||
"rounded-full",
|
||||
props.variant === "secondary" && "border border-border",
|
||||
)}
|
||||
size="sm"
|
||||
variant={props.variant}
|
||||
>
|
||||
<Link
|
||||
href={action.href}
|
||||
target={action.external ? "_blank" : undefined}
|
||||
rel={action.external ? "noopener noreferrer" : undefined}
|
||||
className="flex flex-row items-center gap-2"
|
||||
>
|
||||
{action.icon}
|
||||
{action.label}
|
||||
{action.external && <ArrowUpRightIcon className="size-4" />}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export type ProjectPageHeaderProps = {
|
||||
client: ThirdwebClient;
|
||||
title: string;
|
||||
description?: React.ReactNode;
|
||||
imageUrl?: string | null;
|
||||
icon: React.FC<{ className?: string }>;
|
||||
isProjectIcon?: boolean;
|
||||
actions: {
|
||||
primary: Action;
|
||||
secondary?: Action;
|
||||
} | null;
|
||||
|
||||
links?: ActionLink[];
|
||||
settings?: {
|
||||
href: string;
|
||||
};
|
||||
|
||||
// TODO: add task card component
|
||||
task?: never;
|
||||
};
|
||||
|
||||
export function ProjectPageHeader(props: ProjectPageHeaderProps) {
|
||||
return (
|
||||
<header className="container max-w-7xl py-6 relative">
|
||||
{/* top row */}
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
{/* left - icon */}
|
||||
<div className="flex">
|
||||
{props.isProjectIcon ? (
|
||||
<props.icon />
|
||||
) : (
|
||||
<div className="border rounded-full p-2.5 bg-card">
|
||||
<props.icon className="size-5 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* right - buttons */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
{props.links && props.links.length > 0 && (
|
||||
<LinkGroup links={props.links} />
|
||||
)}
|
||||
|
||||
{props.settings && (
|
||||
<Link href={props.settings.href}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-full gap-2 bg-card"
|
||||
>
|
||||
<Settings2Icon className="size-4 text-muted-foreground" />
|
||||
Settings
|
||||
</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* hide on mobile */}
|
||||
{props.actions && (
|
||||
<div className="hidden lg:flex items-center gap-3">
|
||||
{props.actions.secondary && (
|
||||
<Action action={props.actions.secondary} variant="secondary" />
|
||||
)}
|
||||
|
||||
{props.actions.primary && (
|
||||
<Action action={props.actions.primary} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* mid row */}
|
||||
<div className="space-y-1 max-w-3xl">
|
||||
<h2 className="text-3xl font-semibold tracking-tight">
|
||||
{props.title}
|
||||
</h2>
|
||||
{/* description */}
|
||||
<p className="text-sm text-muted-foreground line-clamp-3 md:line-clamp-2">
|
||||
{props.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* bottom row - hidden on desktop */}
|
||||
{props.actions && (
|
||||
<div className="flex items-center gap-3 lg:hidden">
|
||||
{props.actions?.primary && (
|
||||
<Action action={props.actions.primary} />
|
||||
)}
|
||||
|
||||
{props.actions?.secondary && (
|
||||
<Action action={props.actions.secondary} variant="secondary" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type TabPathLink, TabPathLinks } from "../../ui/tabs";
|
||||
import {
|
||||
ProjectPageFooter,
|
||||
type ProjectPageFooterProps,
|
||||
} from "./project-page-footer";
|
||||
import {
|
||||
ProjectPageHeader,
|
||||
type ProjectPageHeaderProps,
|
||||
} from "./project-page-header";
|
||||
|
||||
type ProjectPageProps = {
|
||||
header: ProjectPageHeaderProps;
|
||||
footer?: ProjectPageFooterProps;
|
||||
/**
|
||||
* @deprecated only for legacy pages where we still need internal tabs for the moment, currently:
|
||||
* - /webhooks
|
||||
*/
|
||||
tabs?: TabPathLink[];
|
||||
};
|
||||
|
||||
export function ProjectPage(props: React.PropsWithChildren<ProjectPageProps>) {
|
||||
return (
|
||||
<div className="flex flex-col grow">
|
||||
<div className={cn(!props.tabs && "border-b")}>
|
||||
<ProjectPageHeader {...props.header} />
|
||||
{props.tabs && (
|
||||
<TabPathLinks
|
||||
scrollableClassName="container max-w-7xl"
|
||||
links={props.tabs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<main className="container max-w-7xl pt-6">{props.children}</main>
|
||||
<div className="h-20" />
|
||||
{props.footer && (
|
||||
<div className="border-t mt-auto">
|
||||
<ProjectPageFooter {...props.footer} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { useShowMore } from "@/hooks/useShowMore";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export function GenericLoadingPage({ className }: { className?: string }) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"use client";
|
||||
import { CheckIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { CheckIcon, CircleSlashIcon, CopyIcon, XIcon } from "lucide-react";
|
||||
import { useMemo } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { Blobbie, type SocialProfile, useSocialProfiles } from "thirdweb/react";
|
||||
@@ -23,6 +23,7 @@ export function WalletAddress(props: {
|
||||
className?: string;
|
||||
iconClassName?: string;
|
||||
client: ThirdwebClient;
|
||||
fallbackIcon?: React.ReactNode;
|
||||
}) {
|
||||
// default back to zero address if no address provided
|
||||
const address = useMemo(() => props.address || ZERO_ADDRESS, [props.address]);
|
||||
@@ -47,7 +48,7 @@ export function WalletAddress(props: {
|
||||
return (
|
||||
<ToolTipLabel hoverable label={address}>
|
||||
<span className="flex items-center gap-2 underline-offset-4 hover:underline">
|
||||
<div className="flex size-6 items-center justify-center rounded-full border bg-background">
|
||||
<div className="flex size-5 items-center justify-center rounded-full border bg-background">
|
||||
<XIcon className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
Invalid Address
|
||||
@@ -59,9 +60,16 @@ export function WalletAddress(props: {
|
||||
// special case for zero address
|
||||
if (address === ZERO_ADDRESS) {
|
||||
return (
|
||||
<span className={cn("cursor-pointer font-mono", props.className)}>
|
||||
{shortenedAddress}
|
||||
</span>
|
||||
<div className="flex items-center gap-2 py-2">
|
||||
<CircleSlashIcon
|
||||
className={cn("size-5 text-muted-foreground/70", props.iconClassName)}
|
||||
/>
|
||||
<span
|
||||
className={cn("cursor-pointer font-mono text-sm", props.className)}
|
||||
>
|
||||
{shortenedAddress}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -82,6 +90,7 @@ export function WalletAddress(props: {
|
||||
iconClassName={props.iconClassName}
|
||||
profiles={profiles.data || []}
|
||||
thirdwebClient={props.client}
|
||||
fallbackIcon={props.fallbackIcon}
|
||||
/>
|
||||
)}
|
||||
<span className="cursor-pointer font-mono">
|
||||
@@ -173,6 +182,7 @@ function WalletAvatar(props: {
|
||||
profiles: SocialProfile[];
|
||||
thirdwebClient: ThirdwebClient;
|
||||
iconClassName?: string;
|
||||
fallbackIcon?: React.ReactNode;
|
||||
}) {
|
||||
const avatar = useMemo(() => {
|
||||
return props.profiles.find(
|
||||
@@ -198,12 +208,21 @@ function WalletAvatar(props: {
|
||||
<Img
|
||||
className={cn("size-5 object-cover", props.iconClassName)}
|
||||
src={resolvedAvatarSrc}
|
||||
fallback={
|
||||
<Blobbie
|
||||
address={props.address}
|
||||
className={props.iconClassName}
|
||||
size={20}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
) : props.fallbackIcon ? (
|
||||
props.fallbackIcon
|
||||
) : (
|
||||
<Blobbie
|
||||
address={props.address}
|
||||
className={props.iconClassName}
|
||||
size={24}
|
||||
size={20}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ThirdwebMiniLogo } from "../../../app/(app)/components/ThirdwebMiniLogo";
|
||||
import { SupportTicketForm } from "../../../app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm";
|
||||
@@ -413,15 +413,15 @@ function StyledMarkdownRenderer(props: {
|
||||
}) {
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
className="[&>*:first-child]:mt-0 [&>*:first-child]:border-none [&>*:first-child]:pb-0 [&>*:last-child]:mb-0"
|
||||
className="text-sm text-foreground [&>*:first-child]:mt-0 [&>*:first-child]:border-none [&>*:first-child]:pb-0 [&>*:last-child]:mb-0 leading-relaxed"
|
||||
code={{
|
||||
className: "bg-card",
|
||||
ignoreFormattingErrors: true,
|
||||
}}
|
||||
inlineCode={{ className: "border-none" }}
|
||||
li={{ className: "text-foreground leading-relaxed" }}
|
||||
markdownText={props.text}
|
||||
p={{
|
||||
className: props.type === "assistant" ? "" : "leading-normal",
|
||||
className: "text-foreground leading-relaxed",
|
||||
}}
|
||||
skipHtml
|
||||
/>
|
||||
|
||||
@@ -22,8 +22,8 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
import { fetchTopContracts, type TrendingContract } from "@/lib/search";
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { SkeletonContainer } from "@/components/ui/skeleton";
|
||||
import {
|
||||
Table,
|
||||
@@ -300,6 +300,7 @@ const contractTypeToAssetTypeRecord: Record<string, string | undefined> = {
|
||||
DropERC20: "Coin",
|
||||
DropERC721: "NFT Collection",
|
||||
DropERC1155: "NFT Collection",
|
||||
ERC20Asset: "Coin",
|
||||
};
|
||||
|
||||
const NetworkFilterCell = React.memo(function NetworkFilterCell({
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import { moduleToBase64 } from "app/(app)/(dashboard)/published-contract/utils/module-base-64";
|
||||
import { RocketIcon, ShieldCheckIcon } from "lucide-react";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import Link from "next/link";
|
||||
import { resolveAvatar } from "thirdweb/extensions/ens";
|
||||
import { fetchPublishedContractVersion } from "@/api/contract/fetch-contracts-with-versions";
|
||||
import { ClientOnly } from "@/components/blocks/client-only";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { serverThirdwebClient } from "@/constants/thirdweb-client.server";
|
||||
import { resolveEns } from "@/lib/ens";
|
||||
import { replaceDeployerAddress } from "@/lib/publisher-utils";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { ContractPublisher } from "./contract-publisher";
|
||||
|
||||
interface ContractCardProps {
|
||||
type ContractCardProps = {
|
||||
publisher: string;
|
||||
contractId: string;
|
||||
titleOverride?: string;
|
||||
@@ -29,7 +30,7 @@ interface ContractCardProps {
|
||||
moduleId: string;
|
||||
version?: string;
|
||||
}[];
|
||||
}
|
||||
};
|
||||
|
||||
function getContractUrl(
|
||||
{
|
||||
@@ -74,6 +75,72 @@ function getContractUrl(
|
||||
return replaceDeployerAddress(pathName);
|
||||
}
|
||||
|
||||
const cached_fetchPublishedContractVersion = unstable_cache(
|
||||
async (publisher: string, contractId: string, version: string = "latest") => {
|
||||
const result = await fetchPublishedContractVersion(
|
||||
publisher,
|
||||
contractId,
|
||||
serverThirdwebClient,
|
||||
version,
|
||||
).catch(() => null);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const publisherEnsAndAvatar = result.publisher
|
||||
? await cached_resolvePublisherEnsAndAvatar(result.publisher)
|
||||
: undefined;
|
||||
|
||||
// Note: Do not return BigInt - it can't be serialized and cached by unstable_cache and will throw an error
|
||||
return {
|
||||
name: result.name,
|
||||
displayName: result.displayName,
|
||||
description: result.description,
|
||||
publisher: {
|
||||
address: result.publisher,
|
||||
ensName: publisherEnsAndAvatar?.ensName,
|
||||
ensAvatar: publisherEnsAndAvatar?.ensAvatar,
|
||||
},
|
||||
version: result.version,
|
||||
audit: result.audit,
|
||||
};
|
||||
},
|
||||
["fetchPublishedContractVersion"],
|
||||
{
|
||||
revalidate: 3600, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
const cached_resolvePublisherEnsAndAvatar = unstable_cache(
|
||||
async (_addressOrEns: string) => {
|
||||
const addressOrEns = replaceDeployerAddress(_addressOrEns);
|
||||
const [ensNameInfo, ensAvatar] = await Promise.allSettled([
|
||||
resolveEns(addressOrEns, serverThirdwebClient),
|
||||
resolveAvatar({
|
||||
client: serverThirdwebClient,
|
||||
name: addressOrEns,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
ensName:
|
||||
ensNameInfo.status === "fulfilled"
|
||||
? ensNameInfo.value?.ensName
|
||||
: undefined,
|
||||
address:
|
||||
ensNameInfo.status === "fulfilled"
|
||||
? ensNameInfo.value?.address
|
||||
: undefined,
|
||||
ensAvatar: ensAvatar.status === "fulfilled" ? ensAvatar.value : undefined,
|
||||
};
|
||||
},
|
||||
["resolvePublisherEnsAndAvatar"],
|
||||
{
|
||||
revalidate: 3600, // 1 hour
|
||||
},
|
||||
);
|
||||
|
||||
export async function ContractCard({
|
||||
publisher,
|
||||
contractId,
|
||||
@@ -83,12 +150,11 @@ export async function ContractCard({
|
||||
modules = [],
|
||||
isBeta,
|
||||
}: ContractCardProps) {
|
||||
const publishedContractResult = await fetchPublishedContractVersion(
|
||||
const publishedContractResult = await cached_fetchPublishedContractVersion(
|
||||
publisher,
|
||||
contractId,
|
||||
serverThirdwebClient,
|
||||
version,
|
||||
).catch(() => null);
|
||||
);
|
||||
|
||||
if (!publishedContractResult) {
|
||||
return null;
|
||||
@@ -186,13 +252,12 @@ export async function ContractCard({
|
||||
!modules?.length && "mt-auto",
|
||||
)}
|
||||
>
|
||||
{publishedContractResult.publisher && (
|
||||
<ClientOnly ssr={<Skeleton className="size-5 rounded-full" />}>
|
||||
<ContractPublisher
|
||||
addressOrEns={publishedContractResult.publisher}
|
||||
client={getClientThirdwebClient()}
|
||||
/>
|
||||
</ClientOnly>
|
||||
{publishedContractResult.publisher.address && (
|
||||
<ContractPublisher
|
||||
address={publishedContractResult.publisher.address}
|
||||
ensName={publishedContractResult.publisher.ensName || undefined}
|
||||
ensAvatar={publishedContractResult.publisher.ensAvatar || undefined}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
|
||||
@@ -1,53 +1,36 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import {
|
||||
AccountAddress,
|
||||
AccountAvatar,
|
||||
AccountBlobbie,
|
||||
AccountName,
|
||||
AccountProvider,
|
||||
} from "thirdweb/react";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { replaceDeployerAddress } from "@/lib/publisher-utils";
|
||||
import { Blobbie } from "thirdweb/react";
|
||||
import { Img } from "@/components/blocks/Img";
|
||||
import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
|
||||
import { resolveSchemeWithErrorHandler } from "@/utils/resolveSchemeWithErrorHandler";
|
||||
import { shortenIfAddress } from "@/utils/usedapp-external";
|
||||
|
||||
interface ContractPublisherProps {
|
||||
addressOrEns: string;
|
||||
client: ThirdwebClient;
|
||||
}
|
||||
|
||||
export const ContractPublisher: React.FC<ContractPublisherProps> = ({
|
||||
addressOrEns,
|
||||
client,
|
||||
}) => {
|
||||
export function ContractPublisher(props: {
|
||||
ensName: string | undefined;
|
||||
address: string;
|
||||
ensAvatar: string | undefined;
|
||||
}) {
|
||||
const displayName = props.ensName || props.address;
|
||||
return (
|
||||
<AccountProvider address={addressOrEns} client={client}>
|
||||
<Link
|
||||
className="flex shrink-0 items-center gap-1.5 hover:underline"
|
||||
href={replaceDeployerAddress(`/${addressOrEns}`)}
|
||||
>
|
||||
<AccountAvatar
|
||||
className="size-5 rounded-full object-cover"
|
||||
fallbackComponent={<AccountBlobbie className="size-5 rounded-full" />}
|
||||
loadingComponent={<Skeleton className="size-5 rounded-full" />}
|
||||
/>
|
||||
<Link
|
||||
className="flex shrink-0 items-center gap-1.5 hover:underline"
|
||||
href={`/${displayName}`}
|
||||
target="_blank"
|
||||
>
|
||||
<Img
|
||||
className="size-5 rounded-full object-cover"
|
||||
src={
|
||||
resolveSchemeWithErrorHandler({
|
||||
client: getClientThirdwebClient(),
|
||||
uri: props.ensAvatar,
|
||||
}) || ""
|
||||
}
|
||||
fallback={<Blobbie address={props.address} />}
|
||||
/>
|
||||
|
||||
<AccountName
|
||||
className="text-xs"
|
||||
fallbackComponent={
|
||||
<AccountAddress
|
||||
className="text-xs"
|
||||
formatFn={(addr) =>
|
||||
shortenIfAddress(replaceDeployerAddress(addr))
|
||||
}
|
||||
/>
|
||||
}
|
||||
formatFn={(name) => replaceDeployerAddress(name)}
|
||||
loadingComponent={<Skeleton className="h-4 w-40" />}
|
||||
/>
|
||||
</Link>
|
||||
</AccountProvider>
|
||||
<span className="text-xs"> {shortenIfAddress(displayName)}</span>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { CodeClient } from "@/components/ui/code/code.client";
|
||||
import { PlainTextCodeBlock } from "@/components/ui/code/plaintext-code";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useChainSlug } from "@/hooks/chains/chainSlug";
|
||||
import { useAddContractToProject } from "@/hooks/project-contracts";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
|
||||
@@ -4,7 +4,7 @@ import { SearchIcon, XIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import type { WalletUser } from "thirdweb/wallets";
|
||||
import { TWTable } from "@/components/blocks/TWTable";
|
||||
import { WalletAddress } from "@/components/blocks/wallet-address";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
ToolTipLabel,
|
||||
Tooltip,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { SettingsIcon } from "lucide-react";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import type { UseNetworkSwitcherModalOptions } from "thirdweb/react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { OPSponsoredChains } from "@/constants/chains";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "@/components/ui/ScrollShadow";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { ChainIconClient } from "@/icons/ChainIcon";
|
||||
import { useStore } from "@/lib/reactive";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { toast } from "sonner";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { FileInput } from "@/components/blocks/FileInput";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useDashboardStorageUpload } from "@/hooks/useDashboardStorageUpload";
|
||||
|
||||
export const IconUpload: React.FC<{
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { DialogDescription } from "@radix-ui/react-dialog";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import type { ProjectService } from "@thirdweb-dev/service-utils";
|
||||
import { SERVICES } from "@thirdweb-dev/service-utils";
|
||||
@@ -18,6 +17,7 @@ import {
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
@@ -32,7 +32,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { createProjectClient } from "@/hooks/useApi";
|
||||
import { useDashboardRouter } from "@/lib/DashboardRouter";
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { isAddress, isValidENSName } from "thirdweb/utils";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { useEns } from "@/hooks/contract-hooks";
|
||||
import type { SolidityInputProps } from ".";
|
||||
import { validateAddress } from "./helpers";
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
import type { ThirdwebClient } from "thirdweb/dist/types/client/client";
|
||||
import type { ThirdwebClient } from "thirdweb";
|
||||
import { analyticsServerProxy } from "@/actions/proxies";
|
||||
import {
|
||||
type SponsoredTransaction,
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
} from "thirdweb";
|
||||
import { type Chain, type ChainMetadata, localhost } from "thirdweb/chains";
|
||||
import {
|
||||
PayEmbed,
|
||||
BuyWidget,
|
||||
useActiveAccount,
|
||||
useActiveWallet,
|
||||
useActiveWalletChain,
|
||||
@@ -29,7 +29,6 @@ import {
|
||||
import { privateKeyToAccount, type Wallet } from "thirdweb/wallets";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DynamicHeight } from "@/components/ui/DynamicHeight";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -42,7 +41,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { LOCAL_NODE_PKEY } from "@/constants/misc";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
import { useV5DashboardChain } from "@/hooks/chains/v5-adapter";
|
||||
@@ -251,39 +250,32 @@ export const MismatchButton = forwardRef<
|
||||
>
|
||||
<DialogContent
|
||||
className={cn(
|
||||
"gap-0 p-0",
|
||||
dialog === "no-funds" && "md:!max-w-[480px]",
|
||||
dialog === "pay" && "md:!max-w-[360px] border-none bg-transparent",
|
||||
"gap-0 p-0 md:!max-w-[480px]",
|
||||
dialog === "pay" && "border-none bg-transparent",
|
||||
)}
|
||||
dialogCloseClassName="focus:ring-0"
|
||||
>
|
||||
<DynamicHeight>
|
||||
{dialog === "no-funds" && (
|
||||
<NoFundsDialogContent
|
||||
chain={txChain}
|
||||
client={props.client}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
onCloseModal={() => setDialog(undefined)}
|
||||
openPayModal={() => {
|
||||
setDialog("pay");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{dialog === "no-funds" && (
|
||||
<NoFundsDialogContent
|
||||
chain={txChain}
|
||||
client={props.client}
|
||||
isLoggedIn={props.isLoggedIn}
|
||||
onCloseModal={() => setDialog(undefined)}
|
||||
openPayModal={() => {
|
||||
setDialog("pay");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{dialog === "pay" && (
|
||||
<PayEmbed
|
||||
className="!w-auto"
|
||||
client={props.client}
|
||||
payOptions={{
|
||||
prefillBuy: {
|
||||
amount: "0.01",
|
||||
chain: txChain,
|
||||
},
|
||||
}}
|
||||
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
|
||||
/>
|
||||
)}
|
||||
</DynamicHeight>
|
||||
{dialog === "pay" && (
|
||||
<BuyWidget
|
||||
className="!w-full"
|
||||
client={props.client}
|
||||
chain={txChain}
|
||||
amount="0.01"
|
||||
theme={getSDKTheme(theme === "dark" ? "dark" : "light")}
|
||||
/>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
@@ -362,11 +354,7 @@ function NoFundsDialogContent(props: {
|
||||
(x) => x.enabled && x.service === "pay",
|
||||
) ? (
|
||||
// pay case
|
||||
<Button
|
||||
className="w-full"
|
||||
onClick={props.openPayModal}
|
||||
variant="primary"
|
||||
>
|
||||
<Button className="w-full" onClick={props.openPayModal}>
|
||||
Buy Funds
|
||||
</Button>
|
||||
) : // no funds options available
|
||||
|
||||
@@ -25,6 +25,7 @@ export function CopyAddressButton(props: {
|
||||
copyIconPosition={props.copyIconPosition}
|
||||
textToCopy={props.address}
|
||||
textToShow={shortenedAddress}
|
||||
iconClassName={props.iconClassName}
|
||||
tooltip={props.tooltip || "Copy Address"}
|
||||
variant={props.variant}
|
||||
/>
|
||||
|
||||
@@ -17,9 +17,7 @@ export function DynamicHeight(props: {
|
||||
boxSizing: "border-box",
|
||||
height: height ? `${height}px` : "auto",
|
||||
overflow: "hidden",
|
||||
transition:
|
||||
props.transition ||
|
||||
"height 210ms cubic-bezier(0.175, 0.885, 0.32, 1.1)",
|
||||
transition: props.transition || "height 220ms ease",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
|
||||
export { ScrollShadow } from "@workspace/ui/components/scroll-shadow";
|
||||
@@ -1,36 +0,0 @@
|
||||
.scrollShadowY {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scrollShadowX {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.scrollShadowTop {
|
||||
top: 0;
|
||||
background: linear-gradient(to bottom, var(--shadow), transparent);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.scrollShadowBottom {
|
||||
bottom: 0;
|
||||
background: linear-gradient(to top, var(--shadow), transparent);
|
||||
}
|
||||
|
||||
.scrollShadowLeft {
|
||||
left: 0;
|
||||
background: linear-gradient(to right, var(--shadow), transparent);
|
||||
}
|
||||
|
||||
.scrollShadowRight {
|
||||
right: 0;
|
||||
background: linear-gradient(to left, var(--shadow), transparent);
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import styles from "./ScrollShadow.module.css";
|
||||
|
||||
export function ScrollShadow(props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
disableTopShadow?: boolean;
|
||||
shadowColor?: string;
|
||||
shadowClassName?: string;
|
||||
}) {
|
||||
const scrollableEl = useRef<HTMLDivElement>(null);
|
||||
const shadowTopEl = useRef<HTMLDivElement>(null);
|
||||
const shadowBottomEl = useRef<HTMLDivElement>(null);
|
||||
const shadowLeftEl = useRef<HTMLDivElement>(null);
|
||||
const shadowRightEl = useRef<HTMLDivElement>(null);
|
||||
const wrapperEl = useRef<HTMLDivElement>(null);
|
||||
|
||||
useIsomorphicLayoutEffect(() => {
|
||||
const content = scrollableEl.current;
|
||||
const shadowTop = shadowTopEl.current;
|
||||
const shadowBottom = shadowBottomEl.current;
|
||||
const wrapper = wrapperEl.current;
|
||||
const shadowLeft = shadowLeftEl.current;
|
||||
const shadowRight = shadowRightEl.current;
|
||||
|
||||
if (!content || !shadowTop || !shadowBottom || !wrapper) {
|
||||
return;
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
if (
|
||||
!content ||
|
||||
!shadowTop ||
|
||||
!shadowBottom ||
|
||||
!shadowLeft ||
|
||||
!shadowRight ||
|
||||
!wrapper
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentScrollHeight = content.scrollHeight - wrapper.offsetHeight;
|
||||
const contentScrollWidth = content.scrollWidth - wrapper.offsetWidth;
|
||||
|
||||
if (contentScrollHeight > 10) {
|
||||
const currentScroll = content.scrollTop / contentScrollHeight;
|
||||
shadowTop.style.opacity = `${currentScroll}`;
|
||||
shadowBottom.style.opacity = `${1 - currentScroll}`;
|
||||
} else {
|
||||
shadowTop.style.opacity = "0";
|
||||
shadowBottom.style.opacity = "0";
|
||||
}
|
||||
|
||||
if (contentScrollWidth > 10) {
|
||||
const currentScrollX = content.scrollLeft / contentScrollWidth;
|
||||
shadowLeft.style.opacity = `${currentScrollX}`;
|
||||
shadowRight.style.opacity = `${1 - currentScrollX}`;
|
||||
} else {
|
||||
shadowLeft.style.opacity = "0";
|
||||
shadowRight.style.opacity = "0";
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
handleScroll();
|
||||
});
|
||||
});
|
||||
content.addEventListener("scroll", handleScroll);
|
||||
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
handleScroll();
|
||||
});
|
||||
|
||||
resizeObserver.observe(content);
|
||||
return () => {
|
||||
content.removeEventListener("scroll", handleScroll);
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(props.className, "relative overflow-hidden")}
|
||||
ref={wrapperEl}
|
||||
style={
|
||||
{
|
||||
"--shadow": props.shadowColor || "hsl(var(--muted))",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
styles.scrollShadowTop,
|
||||
styles.scrollShadowY,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowTopEl}
|
||||
style={{
|
||||
display: props.disableTopShadow ? "none" : "block",
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
styles.scrollShadowBottom,
|
||||
styles.scrollShadowY,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowBottomEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
styles.scrollShadowLeft,
|
||||
styles.scrollShadowX,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowLeftEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn(
|
||||
styles.scrollShadowRight,
|
||||
styles.scrollShadowX,
|
||||
props.shadowClassName,
|
||||
)}
|
||||
ref={shadowRightEl}
|
||||
style={{
|
||||
opacity: "0",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className={cn("no-scrollbar overflow-auto", props.scrollableClassName)}
|
||||
data-scrollable
|
||||
ref={scrollableEl}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { Spinner } from "@workspace/ui/components/spinner";
|
||||
@@ -1,37 +0,0 @@
|
||||
.loader {
|
||||
border-radius: 50%;
|
||||
position: relative;
|
||||
animation: rotate 2s linear infinite;
|
||||
}
|
||||
|
||||
.loader circle {
|
||||
content: "";
|
||||
box-sizing: border-box;
|
||||
position: absolute;
|
||||
inset: 0px;
|
||||
border-radius: 50%;
|
||||
border: 4px solid #fff;
|
||||
stroke-linecap: round;
|
||||
animation: dash 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes rotate {
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes dash {
|
||||
0% {
|
||||
stroke-dasharray: 1, 150;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -35;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 90, 150;
|
||||
stroke-dashoffset: -124;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import style from "./Spinner.module.css";
|
||||
|
||||
export function Spinner(props: { className?: string }) {
|
||||
return (
|
||||
<svg
|
||||
className={cn(style.loader, props.className || "size-4")}
|
||||
viewBox="0 0 50 50"
|
||||
>
|
||||
<title>loading</title>
|
||||
<circle
|
||||
cx="25"
|
||||
cy="25"
|
||||
fill="none"
|
||||
r="20"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1 @@
|
||||
import Link from "next/link";
|
||||
import { cn } from "../../lib/utils";
|
||||
|
||||
type LinkProps = React.ComponentProps<typeof Link>;
|
||||
|
||||
export function UnderlineLink(props: LinkProps) {
|
||||
return (
|
||||
<Link
|
||||
{...props}
|
||||
className={cn(
|
||||
"underline decoration-muted-foreground/50 decoration-dotted underline-offset-[5px] hover:text-foreground hover:decoration-foreground hover:decoration-solid",
|
||||
props.className,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export { UnderlineLink } from "@workspace/ui/components/UnderlineLink";
|
||||
|
||||
@@ -1,39 +1,2 @@
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-full border border-border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 leading-4",
|
||||
{
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
default: "border-transparent bg-primary/20 text-primary",
|
||||
destructive:
|
||||
"border-transparent dark:bg-red-950 dark:text-red-400 bg-red-500/20 text-red-800",
|
||||
outline: "text-foreground",
|
||||
secondary:
|
||||
"border-transparent bg-accent text-accent-foreground hover:bg-accent/80",
|
||||
success:
|
||||
"border-transparent dark:bg-green-950/50 dark:text-green-400 bg-green-200 text-green-950",
|
||||
warning:
|
||||
"border-transparent dark:bg-yellow-600/20 dark:text-yellow-500 bg-yellow-500/20 text-yellow-900",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
);
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants };
|
||||
export type { BadgeProps } from "@workspace/ui/components/badge";
|
||||
export { Badge, badgeVariants } from "@workspace/ui/components/badge";
|
||||
|
||||
@@ -1,119 +1,9 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { ChevronRightIcon, MoreHorizontalIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Breadcrumb = React.forwardRef<
|
||||
HTMLElement,
|
||||
React.ComponentPropsWithoutRef<"nav"> & {
|
||||
separator?: React.ReactNode;
|
||||
}
|
||||
>(({ ...props }, ref) => <nav aria-label="breadcrumb" ref={ref} {...props} />);
|
||||
Breadcrumb.displayName = "Breadcrumb";
|
||||
|
||||
const BreadcrumbList = React.forwardRef<
|
||||
HTMLOListElement,
|
||||
React.ComponentPropsWithoutRef<"ol">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<ol
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-1.5 break-words text-muted-foreground text-sm sm:gap-2.5",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbList.displayName = "BreadcrumbList";
|
||||
|
||||
const BreadcrumbItem = React.forwardRef<
|
||||
HTMLLIElement,
|
||||
React.ComponentPropsWithoutRef<"li">
|
||||
>(({ className, ...props }, ref) => (
|
||||
<li
|
||||
className={cn("inline-flex items-center gap-1.5", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbItem.displayName = "BreadcrumbItem";
|
||||
|
||||
const BreadcrumbLink = React.forwardRef<
|
||||
HTMLAnchorElement,
|
||||
React.ComponentPropsWithoutRef<"a"> & {
|
||||
asChild?: boolean;
|
||||
}
|
||||
>(({ asChild, className, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "a";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
className={cn("transition-colors hover:text-foreground", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
BreadcrumbLink.displayName = "BreadcrumbLink";
|
||||
|
||||
const BreadcrumbPage = React.forwardRef<
|
||||
HTMLSpanElement,
|
||||
React.ComponentPropsWithoutRef<"span">
|
||||
>(({ className, ...props }, ref) => (
|
||||
// biome-ignore lint/a11y/useFocusableInteractive: required
|
||||
<span
|
||||
aria-current="page"
|
||||
aria-disabled="true"
|
||||
className={cn("font-normal text-foreground", className)}
|
||||
ref={ref}
|
||||
role="link"
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
BreadcrumbPage.displayName = "BreadcrumbPage";
|
||||
|
||||
const BreadcrumbSeparator = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"li">) => (
|
||||
<li
|
||||
aria-hidden="true"
|
||||
className={cn("[&>svg]:size-3.5", className)}
|
||||
role="presentation"
|
||||
{...props}
|
||||
>
|
||||
{children ?? <ChevronRightIcon />}
|
||||
</li>
|
||||
);
|
||||
BreadcrumbSeparator.displayName = "BreadcrumbSeparator";
|
||||
|
||||
const BreadcrumbEllipsis = ({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) => (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={cn(
|
||||
"relative flex h-9 w-9 items-center justify-center",
|
||||
className,
|
||||
)}
|
||||
role="presentation"
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="h-4 w-4" />
|
||||
<span className="sr-only">More</span>
|
||||
</span>
|
||||
);
|
||||
BreadcrumbEllipsis.displayName = "BreadcrumbEllipsis";
|
||||
|
||||
export {
|
||||
Breadcrumb,
|
||||
BreadcrumbList,
|
||||
BreadcrumbEllipsis,
|
||||
BreadcrumbItem,
|
||||
BreadcrumbLink,
|
||||
BreadcrumbList,
|
||||
BreadcrumbPage,
|
||||
BreadcrumbSeparator,
|
||||
BreadcrumbEllipsis,
|
||||
};
|
||||
} from "@workspace/ui/components/breadcrumb";
|
||||
|
||||
@@ -1,83 +1,3 @@
|
||||
import { Slot } from "@radix-ui/react-slot";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
defaultVariants: {
|
||||
size: "default",
|
||||
variant: "default",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
icon: "h-10 w-10",
|
||||
lg: "h-11 rounded-md px-8",
|
||||
sm: "h-9 rounded-md px-3",
|
||||
},
|
||||
variant: {
|
||||
default: "bg-foreground text-background hover:bg-foreground/90",
|
||||
destructive:
|
||||
"bg-destructive hover:bg-destructive/90 text-destructive-foreground ",
|
||||
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||
link: "underline-offset-4 hover:underline",
|
||||
outline:
|
||||
"border border-input bg-transparent hover:bg-accent hover:text-accent-foreground",
|
||||
primary: "bg-primary hover:bg-primary/90 text-primary-foreground ",
|
||||
secondary:
|
||||
"bg-secondary hover:bg-secondary/80 text-secondary-foreground ",
|
||||
upsell:
|
||||
"bg-green-600 text-white hover:bg-green-700 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5 transition-all duration-200",
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, asChild = false, disabled, ...props }, ref) => {
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
// "button" elements automatically handle the `disabled` attribute.
|
||||
// For non-button elements rendered via `asChild` (e.g. <a>), we still want
|
||||
// to visually convey the disabled state and prevent user interaction.
|
||||
// We do that by conditionally adding the same utility classes that the
|
||||
// `disabled:` pseudo-variant would normally apply and by setting
|
||||
// `aria-disabled` for accessibility.
|
||||
const disabledClass = disabled ? "pointer-events-none opacity-50" : "";
|
||||
|
||||
const btnOnlyProps =
|
||||
Comp === "button"
|
||||
? {
|
||||
type:
|
||||
(props as React.ButtonHTMLAttributes<HTMLButtonElement>).type ||
|
||||
("button" as const),
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<Comp
|
||||
aria-disabled={disabled ? true : undefined}
|
||||
className={cn(
|
||||
buttonVariants({ className, size, variant }),
|
||||
disabledClass,
|
||||
)}
|
||||
disabled={disabled}
|
||||
ref={ref}
|
||||
{...props}
|
||||
{...btnOnlyProps}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
"use client";
|
||||
export type { ButtonProps } from "@workspace/ui/components/button";
|
||||
export { Button, buttonVariants } from "@workspace/ui/components/button";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { CheckIcon } from "lucide-react";
|
||||
import { CheckIcon, MinusIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -21,7 +21,11 @@ const Checkbox = React.forwardRef<
|
||||
<CheckboxPrimitive.Indicator
|
||||
className={cn("flex items-center justify-center text-current")}
|
||||
>
|
||||
<CheckIcon className="size-4" />
|
||||
{props.checked === "indeterminate" ? (
|
||||
<MinusIcon className="size-3" />
|
||||
) : (
|
||||
<CheckIcon className="size-4" />
|
||||
)}
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
));
|
||||
|
||||
@@ -1,71 +1,3 @@
|
||||
"use client";
|
||||
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|
||||
import type { BundledLanguage } from "shiki";
|
||||
import { getCodeHtml } from "./getCodeHtml";
|
||||
import { PlainTextCodeBlock } from "./plaintext-code";
|
||||
import { RenderCode } from "./RenderCode";
|
||||
|
||||
export type CodeProps = {
|
||||
code: string;
|
||||
lang: BundledLanguage;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
keepPreviousDataOnCodeChange?: boolean;
|
||||
copyButtonClassName?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
ignoreFormattingErrors?: boolean;
|
||||
onCopy?: (code: string) => void;
|
||||
};
|
||||
|
||||
export const CodeClient: React.FC<CodeProps> = ({
|
||||
code,
|
||||
lang,
|
||||
className,
|
||||
scrollableClassName,
|
||||
keepPreviousDataOnCodeChange = false,
|
||||
copyButtonClassName,
|
||||
ignoreFormattingErrors,
|
||||
scrollableContainerClassName,
|
||||
shadowColor,
|
||||
onCopy,
|
||||
}) => {
|
||||
const codeQuery = useQuery({
|
||||
placeholderData: keepPreviousDataOnCodeChange
|
||||
? keepPreviousData
|
||||
: undefined,
|
||||
queryFn: () =>
|
||||
getCodeHtml(code, lang, {
|
||||
ignoreFormattingErrors: ignoreFormattingErrors,
|
||||
}),
|
||||
queryKey: ["html", code],
|
||||
retry: false,
|
||||
});
|
||||
|
||||
if (!codeQuery.data) {
|
||||
return (
|
||||
<PlainTextCodeBlock
|
||||
className={className}
|
||||
code={code}
|
||||
copyButtonClassName={copyButtonClassName}
|
||||
onCopy={onCopy}
|
||||
scrollableClassName={scrollableClassName}
|
||||
scrollableContainerClassName={scrollableContainerClassName}
|
||||
shadowColor={shadowColor}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RenderCode
|
||||
className={className}
|
||||
code={codeQuery.data.formattedCode}
|
||||
copyButtonClassName={copyButtonClassName}
|
||||
html={codeQuery.data.html}
|
||||
onCopy={onCopy}
|
||||
scrollableClassName={scrollableClassName}
|
||||
scrollableContainerClassName={scrollableContainerClassName}
|
||||
shadowColor={shadowColor}
|
||||
/>
|
||||
);
|
||||
};
|
||||
export { CodeClient } from "@workspace/ui/components/code/code.client";
|
||||
|
||||
@@ -1,22 +1,4 @@
|
||||
import type { BundledLanguage } from "shiki";
|
||||
import { getCodeHtml } from "./getCodeHtml";
|
||||
import { RenderCode } from "./RenderCode";
|
||||
|
||||
export type CodeProps = {
|
||||
code: string;
|
||||
lang: BundledLanguage;
|
||||
className?: string;
|
||||
ignoreFormattingErrors?: boolean;
|
||||
};
|
||||
|
||||
export const CodeServer: React.FC<CodeProps> = async ({
|
||||
code,
|
||||
lang,
|
||||
className,
|
||||
ignoreFormattingErrors,
|
||||
}) => {
|
||||
const { html, formattedCode } = await getCodeHtml(code, lang, {
|
||||
ignoreFormattingErrors,
|
||||
});
|
||||
return <RenderCode className={className} code={formattedCode} html={html} />;
|
||||
};
|
||||
export {
|
||||
type CodeProps,
|
||||
CodeServer,
|
||||
} from "@workspace/ui/components/code/code.server";
|
||||
|
||||
@@ -1,29 +1 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CodeBlockContainer } from "./CodeBlockContainer";
|
||||
|
||||
export function PlainTextCodeBlock(props: {
|
||||
code: string;
|
||||
copyButtonClassName?: string;
|
||||
className?: string;
|
||||
scrollableClassName?: string;
|
||||
codeClassName?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
onCopy?: (code: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<CodeBlockContainer
|
||||
className={props.className}
|
||||
codeToCopy={props.code}
|
||||
copyButtonClassName={props.copyButtonClassName}
|
||||
onCopy={props.onCopy}
|
||||
scrollableClassName={props.scrollableClassName}
|
||||
scrollableContainerClassName={props.scrollableContainerClassName}
|
||||
shadowColor={props.shadowColor}
|
||||
>
|
||||
<code className={cn("block whitespace-pre", props.codeClassName)}>
|
||||
{props.code}
|
||||
</code>
|
||||
</CodeBlockContainer>
|
||||
);
|
||||
}
|
||||
export { PlainTextCodeBlock } from "@workspace/ui/components/code/plaintext-code";
|
||||
|
||||
@@ -1,25 +1 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
return (
|
||||
<input
|
||||
className={cn(
|
||||
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background selection:bg-inverted selection:text-inverted-foreground file:border-0 file:bg-transparent file:font-medium file:text-sm placeholder:text-muted-foreground placeholder:text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
type={type}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
export { Input } from "@workspace/ui/components/input";
|
||||
|
||||
@@ -1,156 +1,11 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
|
||||
|
||||
const Table = React.forwardRef<
|
||||
HTMLTableElement,
|
||||
React.HTMLAttributes<HTMLTableElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<table
|
||||
className={cn(
|
||||
"w-full caption-bottom border-collapse align-top text-sm lining-nums tabular-nums",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
Table.displayName = "Table";
|
||||
|
||||
const TableHeader = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<thead
|
||||
className={cn("relative border-b bg-background", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
||||
TableHeader.displayName = "TableHeader";
|
||||
|
||||
const TableBody = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tbody
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableBody.displayName = "TableBody";
|
||||
|
||||
const TableFooter = React.forwardRef<
|
||||
HTMLTableSectionElement,
|
||||
React.HTMLAttributes<HTMLTableSectionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<tfoot
|
||||
className={cn(
|
||||
"border-border border-t bg-card font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableFooter.displayName = "TableFooter";
|
||||
|
||||
const TableRow = React.forwardRef<
|
||||
HTMLTableRowElement,
|
||||
React.HTMLAttributes<HTMLTableRowElement> & {
|
||||
/**
|
||||
* Contain the absolutely position elements inside the row with position:relative + transform:translate(0)
|
||||
* transform:translate(0) is required because position:relative on tr element does not work on webkit
|
||||
*/
|
||||
linkBox?: boolean;
|
||||
}
|
||||
>(({ className, linkBox, ...props }, ref) => (
|
||||
<tr
|
||||
className={cn(
|
||||
"border-border border-b last:border-0 data-[state=selected]:bg-muted",
|
||||
linkBox && "relative translate-x-0 translate-y-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableRow.displayName = "TableRow";
|
||||
|
||||
const TableHead = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.ThHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<th
|
||||
className={cn(
|
||||
"px-6 py-4 text-left align-middle font-medium text-muted-foreground text-xs uppercase tracking-wider [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableHead.displayName = "TableHead";
|
||||
|
||||
const TableCell = React.forwardRef<
|
||||
HTMLTableCellElement,
|
||||
React.TdHTMLAttributes<HTMLTableCellElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<td
|
||||
className={cn(
|
||||
"px-6 py-4 text-start align-middle text-sm [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCell.displayName = "TableCell";
|
||||
|
||||
const TableCaption = React.forwardRef<
|
||||
HTMLTableCaptionElement,
|
||||
React.HTMLAttributes<HTMLTableCaptionElement>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<caption
|
||||
className={cn("py-4 text-muted-foreground text-sm", className)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TableCaption.displayName = "TableCaption";
|
||||
|
||||
function TableContainer(props: {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
scrollableContainerClassName?: string;
|
||||
}) {
|
||||
return (
|
||||
<ScrollShadow
|
||||
className={cn(
|
||||
"relative whitespace-nowrap rounded-lg border border-border bg-card",
|
||||
props.className,
|
||||
)}
|
||||
disableTopShadow
|
||||
scrollableClassName={props.scrollableContainerClassName}
|
||||
shadowClassName="z-30"
|
||||
shadowColor="hsl(var(--muted))"
|
||||
>
|
||||
{props.children}
|
||||
</ScrollShadow>
|
||||
);
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableContainer,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
TableContainer,
|
||||
};
|
||||
} from "@workspace/ui/components/table";
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useCallback, useRef, useState } from "react";
|
||||
import { useIsomorphicLayoutEffect } from "@/lib/useIsomorphicLayoutEffect";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "./button";
|
||||
import { ScrollShadow } from "./ScrollShadow/ScrollShadow";
|
||||
import { ScrollShadow } from "./ScrollShadow";
|
||||
import { ToolTipLabel } from "./tooltip";
|
||||
|
||||
export type TabLink = {
|
||||
@@ -98,6 +98,7 @@ export function TabButtons(props: {
|
||||
shadowColor?: string;
|
||||
tabIconClassName?: string;
|
||||
hideBottomLine?: boolean;
|
||||
bottomLineClassName?: string;
|
||||
}) {
|
||||
const { containerRef, lineRef, activeTabRef } =
|
||||
useUnderline<HTMLButtonElement>();
|
||||
@@ -106,7 +107,12 @@ export function TabButtons(props: {
|
||||
<div className={cn("relative", props.containerClassName)}>
|
||||
{/* Bottom line */}
|
||||
{!props.hideBottomLine && (
|
||||
<div className="absolute right-0 bottom-0 left-0 h-[1px] bg-border" />
|
||||
<div
|
||||
className={cn(
|
||||
"absolute right-0 bottom-0 left-0 h-[1px] bg-border",
|
||||
props.bottomLineClassName,
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ScrollShadow
|
||||
@@ -210,14 +216,16 @@ function useUnderline<El extends HTMLElement>() {
|
||||
return { activeTabRef, containerRef, lineRef };
|
||||
}
|
||||
|
||||
export type TabPathLink = {
|
||||
name: React.ReactNode;
|
||||
path: string;
|
||||
exactMatch?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isActive?: (pathname: string) => boolean;
|
||||
};
|
||||
|
||||
export function TabPathLinks(props: {
|
||||
links: {
|
||||
name: React.ReactNode;
|
||||
path: string;
|
||||
exactMatch?: boolean;
|
||||
isDisabled?: boolean;
|
||||
isActive?: (pathname: string) => boolean;
|
||||
}[];
|
||||
links: TabPathLink[];
|
||||
className?: string;
|
||||
tabContainerClassName?: string;
|
||||
shadowColor?: string;
|
||||
|
||||
@@ -1,70 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider;
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root;
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger;
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Content
|
||||
className={cn(
|
||||
"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,
|
||||
)}
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName;
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };
|
||||
|
||||
export function ToolTipLabel(props: {
|
||||
children: React.ReactNode;
|
||||
label: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
leftIcon?: React.ReactNode;
|
||||
hoverable?: boolean;
|
||||
contentClassName?: string;
|
||||
side?: "top" | "right" | "bottom" | "left";
|
||||
align?: "center" | "start" | "end";
|
||||
}) {
|
||||
if (!props.label) {
|
||||
return props.children;
|
||||
}
|
||||
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<Tooltip delayDuration={100} disableHoverableContent={!props.hoverable}>
|
||||
<TooltipTrigger asChild className="!pointer-events-auto" tabIndex={-1}>
|
||||
{props.children}
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
align={props.align}
|
||||
className={cn(
|
||||
"max-w-[400px] whitespace-normal p-0 leading-relaxed",
|
||||
props.contentClassName,
|
||||
)}
|
||||
side={props.side}
|
||||
sideOffset={10}
|
||||
>
|
||||
<div className="flex items-center gap-1.5 p-4 text-sm">
|
||||
{props.leftIcon}
|
||||
{props.label}
|
||||
{props.rightIcon}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
||||
export {
|
||||
ToolTipLabel,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@workspace/ui/components/tooltip";
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
export const THIRDWEB_DEPLOYER_ADDRESS =
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
|
||||
|
||||
export const THIRDWEB_PUBLISHER_ADDRESS =
|
||||
"0xdd99b75f095d0c4d5112aCe938e4e6ed962fb024";
|
||||
|
||||
export const DEFAULT_FEE_RECIPIENT =
|
||||
"0x1af20c6b23373350ad464700b5965ce4b0d2ad94";
|
||||
|
||||
|
||||
@@ -3,3 +3,4 @@ export const COOKIE_PREFIX_TOKEN = "tw_token_";
|
||||
|
||||
export const LAST_USED_PROJECT_ID = "last-used-project-id";
|
||||
export const LAST_USED_TEAM_ID = "last-used-team-id";
|
||||
export const HAS_USED_DASHBOARD = "has-used-dashboard";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client";
|
||||
|
||||
import { CircleAlertIcon, InfoIcon } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { createContext, useCallback, useContext, useState } from "react";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useCallback, useMemo } from "react";
|
||||
import type { Chain, ChainMetadata } from "thirdweb/chains";
|
||||
import { useActiveWalletChain } from "thirdweb/react";
|
||||
import { useAllChainsData } from "@/hooks/chains/allChains";
|
||||
@@ -27,6 +27,17 @@ export function useV5DashboardChain(
|
||||
}, [chainId, idToChain]);
|
||||
}
|
||||
|
||||
export function useGetV5DashboardChain() {
|
||||
const { idToChain } = useAllChainsData();
|
||||
return useCallback(
|
||||
(chainId: number) => {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return defineDashboardChain(chainId, idToChain.get(chainId));
|
||||
},
|
||||
[idToChain],
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* same behavior as v4 `useChain()` but for v5
|
||||
*/
|
||||
|
||||
@@ -11,7 +11,12 @@ export function useAddContractToProject() {
|
||||
contractAddress: string;
|
||||
chainId: string;
|
||||
deploymentType: "asset" | undefined;
|
||||
contractType: "DropERC20" | "DropERC721" | "DropERC1155" | undefined;
|
||||
contractType:
|
||||
| "ERC20Asset"
|
||||
| "DropERC721"
|
||||
| "DropERC1155"
|
||||
| "DropERC20"
|
||||
| undefined;
|
||||
}) => {
|
||||
const res = await apiServerProxy({
|
||||
body: JSON.stringify({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { getUniversalBridgeTokens } from "@/api/universal-bridge/tokens";
|
||||
import { getUniversalBridgeTokens } from "@/api/universal-bridge/token-list";
|
||||
|
||||
export function useTokensData({
|
||||
chainId,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import pLimit from "p-limit";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import Papa from "papaparse";
|
||||
import { useCallback, useState } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
@@ -95,6 +94,7 @@ export function useCsvUpload<
|
||||
// Always gonna need the wallet address
|
||||
T extends { address: string },
|
||||
>(props: Props<T>) {
|
||||
const queryClient = useQueryClient();
|
||||
const [rawData, setRawData] = useState<
|
||||
T[] | Array<T & { [key in string]: unknown }>
|
||||
>(props.defaultRawData || []);
|
||||
@@ -132,16 +132,29 @@ export function useCsvUpload<
|
||||
[props.csvParser],
|
||||
);
|
||||
|
||||
const [normalizeProgress, setNormalizeProgress] = useState({
|
||||
total: 0,
|
||||
current: 0,
|
||||
});
|
||||
|
||||
const normalizeQuery = useQuery({
|
||||
queryFn: async () => {
|
||||
const limit = pLimit(50);
|
||||
const results = await Promise.all(
|
||||
rawData.map((item) => {
|
||||
return limit(() =>
|
||||
const batchSize = 50;
|
||||
const results = [];
|
||||
for (let i = 0; i < rawData.length; i += batchSize) {
|
||||
const batch = rawData.slice(i, i + batchSize);
|
||||
setNormalizeProgress({
|
||||
total: rawData.length,
|
||||
current: i,
|
||||
});
|
||||
const batchResults = await Promise.all(
|
||||
batch.map((item) =>
|
||||
checkIsAddress({ item: item, thirdwebClient: props.client }),
|
||||
);
|
||||
}),
|
||||
);
|
||||
),
|
||||
);
|
||||
results.push(...batchResults);
|
||||
}
|
||||
|
||||
return {
|
||||
invalidFound: !!results.find((item) => !item?.isValid),
|
||||
result: processAirdropData(results),
|
||||
@@ -153,12 +166,18 @@ export function useCsvUpload<
|
||||
|
||||
const removeInvalid = useCallback(() => {
|
||||
const filteredData = normalizeQuery.data?.result.filter(
|
||||
({ isValid }) => isValid,
|
||||
(d) => d.isValid && d.resolvedAddress !== ZERO_ADDRESS,
|
||||
);
|
||||
// double type assertion is save here because we don't really use this variable (only check for its length)
|
||||
// Also filteredData's type is the superset of T[]
|
||||
setRawData(filteredData as unknown as T[]);
|
||||
}, [normalizeQuery.data?.result]);
|
||||
|
||||
if (filteredData && normalizeQuery.data) {
|
||||
// Directly update the query result instead of setting new state to avoid triggering refetch
|
||||
queryClient.setQueryData(["snapshot-check-isAddress", rawData], {
|
||||
...normalizeQuery.data,
|
||||
result: filteredData,
|
||||
invalidFound: false, // Since we removed all invalid items
|
||||
});
|
||||
}
|
||||
}, [normalizeQuery.data, queryClient, rawData]);
|
||||
|
||||
const processData = useCallback(
|
||||
(data: T[]) => {
|
||||
@@ -181,5 +200,6 @@ export function useCsvUpload<
|
||||
removeInvalid,
|
||||
reset,
|
||||
setFiles,
|
||||
normalizeProgress,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import { useActiveAccount } from "thirdweb/react";
|
||||
import invariant from "tiny-invariant";
|
||||
import { apiServerProxy } from "@/actions/proxies";
|
||||
import type { EngineBackendWalletType } from "@/constants/engine";
|
||||
import type { ResultItem } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineStatus } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/dedicated/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import type { ResultItem } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/metrics/components/StatusCodes";
|
||||
import type { EngineStatus } from "../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/engine/(instance)/[engineId]/overview/components/transactions-table";
|
||||
import { engineKeys } from "../query-keys/cache-keys";
|
||||
|
||||
// Engine instances
|
||||
@@ -1662,7 +1662,7 @@ export function useEngineSystemMetrics(
|
||||
queryFn: async () => {
|
||||
const res = await apiServerProxy({
|
||||
method: "GET",
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/${projectSlug}/engine/dedicated/${engineId}/metrics`,
|
||||
pathname: `/v1/teams/${teamIdOrSlug}/${projectSlug}/engine/${engineId}/metrics`,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
|
||||
@@ -7,15 +7,20 @@ import {
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
type Chain,
|
||||
getAddress,
|
||||
NATIVE_TOKEN_ADDRESS,
|
||||
sendAndConfirmTransaction,
|
||||
type ThirdwebClient,
|
||||
type ThirdwebContract,
|
||||
} from "thirdweb";
|
||||
import type { GetBalanceResult } from "thirdweb/extensions/erc20";
|
||||
import { distribute, distributeByToken } from "thirdweb/extensions/split";
|
||||
import { getOwnedTokens } from "thirdweb/insight";
|
||||
import { useActiveAccount } from "thirdweb/react";
|
||||
import { getWalletBalance } from "thirdweb/wallets";
|
||||
import invariant from "tiny-invariant";
|
||||
import { parseError } from "../utils/errorParser";
|
||||
import { tryCatch } from "../utils/try-catch";
|
||||
|
||||
function getTokenBalancesQuery(params: {
|
||||
ownerAddress: string;
|
||||
@@ -24,7 +29,7 @@ function getTokenBalancesQuery(params: {
|
||||
}) {
|
||||
return queryOptions({
|
||||
queryFn: async () => {
|
||||
return getOwnedTokens({
|
||||
const ownedTokenBalancePromise = getOwnedTokens({
|
||||
client: params.client,
|
||||
chains: [params.chain],
|
||||
ownerAddress: params.ownerAddress,
|
||||
@@ -32,6 +37,31 @@ function getTokenBalancesQuery(params: {
|
||||
include_native: "true",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await tryCatch(ownedTokenBalancePromise);
|
||||
|
||||
// fallback to fetch native token balance with rpc
|
||||
if (result.error) {
|
||||
const walletBalance = await getWalletBalance({
|
||||
address: params.ownerAddress,
|
||||
client: params.client,
|
||||
chain: params.chain,
|
||||
});
|
||||
|
||||
const nativeTokenBalance: GetBalanceResult = {
|
||||
name: walletBalance.name,
|
||||
value: walletBalance.value,
|
||||
decimals: walletBalance.decimals,
|
||||
displayValue: walletBalance.displayValue,
|
||||
symbol: walletBalance.symbol,
|
||||
chainId: params.chain.id,
|
||||
tokenAddress: NATIVE_TOKEN_ADDRESS,
|
||||
};
|
||||
|
||||
return [nativeTokenBalance];
|
||||
}
|
||||
|
||||
return result.data;
|
||||
},
|
||||
queryKey: ["getOwnedTokens", params.chain.id, params.ownerAddress],
|
||||
retry: false,
|
||||
@@ -69,7 +99,8 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
.filter((token) => token.value !== 0n)
|
||||
.map(async (currency) => {
|
||||
const transaction =
|
||||
currency.name === "Native Token"
|
||||
getAddress(currency.tokenAddress) ===
|
||||
getAddress(NATIVE_TOKEN_ADDRESS)
|
||||
? distribute({ contract })
|
||||
: distributeByToken({
|
||||
contract,
|
||||
@@ -79,6 +110,7 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
account,
|
||||
transaction,
|
||||
});
|
||||
|
||||
toast.promise(promise, {
|
||||
error: (err) => ({
|
||||
message: `Error distributing ${currency.name}`,
|
||||
@@ -87,6 +119,8 @@ export function useSplitDistributeFunds(contract: ThirdwebContract) {
|
||||
loading: `Distributing ${currency.name}`,
|
||||
success: `Successfully distributed ${currency.name}`,
|
||||
});
|
||||
|
||||
await promise;
|
||||
});
|
||||
|
||||
return await Promise.all(distributions);
|
||||
|
||||
@@ -74,6 +74,12 @@ export function teamStub(id: string, billingPlan: Team["billingPlan"]): Team {
|
||||
rpc: {
|
||||
enabled: true,
|
||||
rateLimit: 1000,
|
||||
websockets: {
|
||||
enabled: false,
|
||||
reasonCode: "enterprise_plan_required",
|
||||
maxConnections: 0,
|
||||
maxSubscriptions: 0,
|
||||
},
|
||||
},
|
||||
storage: {
|
||||
download: {
|
||||
|
||||
@@ -7,17 +7,11 @@ export interface WalletStats {
|
||||
walletType: string;
|
||||
}
|
||||
|
||||
export interface WalletUserStats {
|
||||
date: string;
|
||||
newUsers: number;
|
||||
returningUsers: number;
|
||||
totalUsers: number;
|
||||
}
|
||||
|
||||
export interface InAppWalletStats {
|
||||
date: string;
|
||||
authenticationMethod: string;
|
||||
uniqueWalletsConnected: number;
|
||||
newUsers: number;
|
||||
}
|
||||
|
||||
export interface EcosystemWalletStats extends InAppWalletStats {
|
||||
@@ -66,13 +60,6 @@ export interface UniversalBridgeWalletStats {
|
||||
developerFeeUsdCents: number;
|
||||
}
|
||||
|
||||
export interface WebhookRequestStats {
|
||||
date: string;
|
||||
webhookId: string;
|
||||
httpStatusCode: number;
|
||||
totalRequests: number;
|
||||
}
|
||||
|
||||
export interface WebhookLatencyStats {
|
||||
date: string;
|
||||
webhookId: string;
|
||||
@@ -91,6 +78,14 @@ export interface WebhookSummaryStats {
|
||||
errorBreakdown: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface AIUsageStats {
|
||||
date: string;
|
||||
totalPromptTokens: number;
|
||||
totalCompletionTokens: number;
|
||||
totalSessions: number;
|
||||
totalRequests: number;
|
||||
}
|
||||
|
||||
export interface AnalyticsQueryParams {
|
||||
teamId: string;
|
||||
projectId?: string;
|
||||
|
||||
+1
-1
@@ -43,7 +43,7 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
NEXT_PUBLIC_THIRDWEB_ENGINE_FAUCET_WALLET,
|
||||
NEXT_PUBLIC_TURNSTILE_SITE_KEY,
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import {
|
||||
import { useDebounce } from "use-debounce";
|
||||
import { useCustomConnectModal } from "@/components/connect-wallet";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
|
||||
type AddChainToWalletProps = {
|
||||
|
||||
+1
-2
@@ -1,5 +1,4 @@
|
||||
// FIXME: export listing status type
|
||||
import type { ListingStatus } from "thirdweb/dist/types/extensions/marketplace/types";
|
||||
import type { ListingStatus } from "thirdweb/extensions/marketplace";
|
||||
|
||||
export const LISTING_STATUS: Record<ListingStatus, string> = {
|
||||
ACTIVE: "Active",
|
||||
|
||||
+10
-11
@@ -27,13 +27,15 @@ export const ClaimerSelection = () => {
|
||||
setOpenSnapshotIndex: setOpenIndex,
|
||||
isAdmin,
|
||||
claimConditionType,
|
||||
phaseSnapshots,
|
||||
setPhaseSnapshot,
|
||||
} = useClaimConditionsFormContext();
|
||||
|
||||
const handleClaimerChange = (value: string) => {
|
||||
const val = value as "any" | "specific" | "overrides";
|
||||
|
||||
if (val === "any") {
|
||||
form.setValue(`phases.${phaseIndex}.snapshot`, undefined);
|
||||
setPhaseSnapshot(phaseIndex, undefined);
|
||||
} else {
|
||||
if (val === "specific") {
|
||||
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 0);
|
||||
@@ -41,7 +43,7 @@ export const ClaimerSelection = () => {
|
||||
if (val === "overrides" && field.maxClaimablePerWallet !== 1) {
|
||||
form.setValue(`phases.${phaseIndex}.maxClaimablePerWallet`, 1);
|
||||
}
|
||||
form.setValue(`phases.${phaseIndex}.snapshot`, []);
|
||||
setPhaseSnapshot(phaseIndex, []);
|
||||
setOpenIndex(phaseIndex);
|
||||
}
|
||||
};
|
||||
@@ -49,6 +51,7 @@ export const ClaimerSelection = () => {
|
||||
let helperText: React.ReactNode;
|
||||
|
||||
const disabledSnapshotButton = isAdmin && formDisabled;
|
||||
const snapshot = phaseSnapshots[phaseIndex];
|
||||
|
||||
if (dropType === "specific") {
|
||||
helperText = (
|
||||
@@ -87,10 +90,7 @@ export const ClaimerSelection = () => {
|
||||
|
||||
return (
|
||||
<FormFieldSetup
|
||||
errorMessage={
|
||||
form.getFieldState(`phases.${phaseIndex}.snapshot`, form.formState)
|
||||
?.error?.message
|
||||
}
|
||||
errorMessage={undefined}
|
||||
helperText={helperText}
|
||||
label={label}
|
||||
isRequired={false}
|
||||
@@ -117,7 +117,7 @@ export const ClaimerSelection = () => {
|
||||
)}
|
||||
|
||||
{/* Edit or See Snapshot */}
|
||||
{field.snapshot ? (
|
||||
{snapshot ? (
|
||||
<div className="flex items-center gap-3">
|
||||
{/* disable the "Edit" button when form is disabled, but not when it's a "See" button */}
|
||||
<Button
|
||||
@@ -133,7 +133,7 @@ export const ClaimerSelection = () => {
|
||||
<div
|
||||
className={cn(
|
||||
"flex gap-2 items-center",
|
||||
field.snapshot?.length === 0
|
||||
snapshot?.length === 0
|
||||
? "text-muted-foreground"
|
||||
: "text-green-600 dark:text-green-500",
|
||||
disabledSnapshotButton ? "opacity-50" : "",
|
||||
@@ -141,9 +141,8 @@ export const ClaimerSelection = () => {
|
||||
>
|
||||
<div className="size-2 bg-current rounded-full" />
|
||||
<span className="text-sm">
|
||||
{field.snapshot?.length}{" "}
|
||||
{field.snapshot?.length === 1 ? "address" : "addresses"} in
|
||||
snapshot
|
||||
{snapshot?.length}{" "}
|
||||
{snapshot?.length === 1 ? "address" : "addresses"} in snapshot
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+10
-16
@@ -5,13 +5,12 @@ import {
|
||||
toUnits,
|
||||
} from "thirdweb";
|
||||
import { getContract } from "thirdweb/contract";
|
||||
import type { OverrideEntry } from "thirdweb/dist/types/utils/extensions/drops/types";
|
||||
import type { Prettify } from "thirdweb/dist/types/utils/type-utils";
|
||||
import { getContractMetadata } from "thirdweb/extensions/common";
|
||||
import * as ERC20Ext from "thirdweb/extensions/erc20";
|
||||
import * as ERC721Ext from "thirdweb/extensions/erc721";
|
||||
import * as ERC1155Ext from "thirdweb/extensions/erc1155";
|
||||
import { download } from "thirdweb/storage";
|
||||
import type { OverrideEntry } from "thirdweb/utils";
|
||||
import { maxUint256 } from "thirdweb/utils";
|
||||
import type { z } from "zod";
|
||||
import type {
|
||||
@@ -26,20 +25,15 @@ type SnapshotEntry = {
|
||||
currencyAddress?: string | undefined;
|
||||
};
|
||||
|
||||
type CombinedClaimCondition = Prettify<
|
||||
Omit<
|
||||
LegacyClaimCondition,
|
||||
"price" | "waitInSeconds" | "availableSupply" | "currencyMetadata"
|
||||
> & {
|
||||
price: bigint;
|
||||
currencyMetadata: Omit<
|
||||
LegacyClaimCondition["currencyMetadata"],
|
||||
"value"
|
||||
> & {
|
||||
value: bigint;
|
||||
};
|
||||
}
|
||||
>;
|
||||
type CombinedClaimCondition = Omit<
|
||||
LegacyClaimCondition,
|
||||
"price" | "waitInSeconds" | "availableSupply" | "currencyMetadata"
|
||||
> & {
|
||||
price: bigint;
|
||||
currencyMetadata: Omit<LegacyClaimCondition["currencyMetadata"], "value"> & {
|
||||
value: bigint;
|
||||
};
|
||||
};
|
||||
|
||||
type Options =
|
||||
| {
|
||||
|
||||
+117
-63
@@ -6,7 +6,15 @@ import {
|
||||
CircleHelpIcon,
|
||||
PlusIcon,
|
||||
} from "lucide-react";
|
||||
import { createContext, Fragment, useContext, useMemo, useState } from "react";
|
||||
import {
|
||||
createContext,
|
||||
Fragment,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
type UseFieldArrayReturn,
|
||||
type UseFormReturn,
|
||||
@@ -38,7 +46,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Form } from "@/components/ui/form";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useIsAdmin } from "@/hooks/useContractRoles";
|
||||
import { useTxNotifications } from "@/hooks/useTxNotifications";
|
||||
@@ -120,21 +128,19 @@ export const ClaimConditionTypeData: Record<
|
||||
});
|
||||
|
||||
const getClaimConditionTypeFromPhase = (
|
||||
phase: ClaimConditionInput,
|
||||
phase: Omit<ClaimConditionInput, "snapshot">,
|
||||
snapshot: SnapshotEntry[] | undefined,
|
||||
): ClaimConditionType => {
|
||||
if (!phase.snapshot) {
|
||||
if (!snapshot) {
|
||||
return "public";
|
||||
}
|
||||
|
||||
if (phase.snapshot) {
|
||||
if (snapshot) {
|
||||
if (
|
||||
phase.maxClaimablePerWallet?.toString() === "0" &&
|
||||
phase.price === "0" &&
|
||||
typeof phase.snapshot !== "string" &&
|
||||
phase.snapshot.length === 1 &&
|
||||
phase.snapshot.some(
|
||||
(a) => (a as SnapshotEntry).maxClaimable === "unlimited",
|
||||
)
|
||||
snapshot.length === 1 &&
|
||||
snapshot.some((a) => a.maxClaimable === "unlimited")
|
||||
) {
|
||||
return "creator";
|
||||
}
|
||||
@@ -165,6 +171,11 @@ interface ClaimsConditionFormContextData {
|
||||
isColumn?: boolean;
|
||||
isErc20: boolean;
|
||||
claimConditionType: ClaimConditionType;
|
||||
phaseSnapshots: Record<number, SnapshotEntry[] | undefined>;
|
||||
setPhaseSnapshot: (
|
||||
phaseIndex: number,
|
||||
snapshot: SnapshotEntry[] | undefined,
|
||||
) => void;
|
||||
}
|
||||
|
||||
// legacy, but we SHOULD remove this and instead pass down props!
|
||||
@@ -207,6 +218,10 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
|
||||
const isAdmin = useIsAdmin(contract);
|
||||
const [openSnapshotIndex, setOpenSnapshotIndex] = useState(-1);
|
||||
const [phaseSnapshots, setPhaseSnapshots] = useState<
|
||||
Record<number, SnapshotEntry[] | undefined>
|
||||
>({});
|
||||
|
||||
const sendTx = useSendAndConfirmTransaction();
|
||||
|
||||
const tokenDecimals = useReadContract(decimals, {
|
||||
@@ -233,35 +248,56 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
}),
|
||||
});
|
||||
|
||||
const isSnapshotInitialized = useRef(false);
|
||||
// save snapshots in a separate state instead of form state because it makes rendering very slow for large snapshots (10k, 100k rows etc)
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
useEffect(() => {
|
||||
if (isSnapshotInitialized.current || !claimConditionsQuery.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newPhaseSnapshots: Record<number, SnapshotEntry[] | undefined> = {};
|
||||
|
||||
claimConditionsQuery.data.forEach((phase, idx) => {
|
||||
const snapshot = phase.snapshot?.map(
|
||||
({ address, maxClaimable, price, currencyAddress }) => ({
|
||||
address,
|
||||
currencyAddress: currencyAddress || undefined,
|
||||
maxClaimable: maxClaimable || "0",
|
||||
price: price || undefined,
|
||||
}),
|
||||
);
|
||||
newPhaseSnapshots[idx] = snapshot;
|
||||
});
|
||||
|
||||
setPhaseSnapshots(newPhaseSnapshots);
|
||||
isSnapshotInitialized.current = true;
|
||||
}, [claimConditionsQuery.data]);
|
||||
|
||||
const transformedQueryData = useMemo(() => {
|
||||
return (claimConditionsQuery.data || [])
|
||||
.map((phase, idx) => ({
|
||||
...phase,
|
||||
currencyAddress: phase.currencyAddress?.toLowerCase() || "0",
|
||||
currencyMetadata: {
|
||||
...phase.currencyMetadata,
|
||||
value: phase.currencyMetadata.value?.toString() || "0",
|
||||
},
|
||||
fromSdk: true,
|
||||
isEditing: false,
|
||||
maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0",
|
||||
maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0",
|
||||
merkleRootHash: (phase.merkleRootHash || "") as string,
|
||||
metadata: {
|
||||
...phase.metadata,
|
||||
name: phase?.metadata?.name || `Phase ${idx + 1}`,
|
||||
},
|
||||
price: phase.currencyMetadata.displayValue,
|
||||
snapshot: phase.snapshot?.map(
|
||||
({ address, maxClaimable, price, currencyAddress }) => ({
|
||||
address,
|
||||
currencyAddress: currencyAddress || undefined,
|
||||
maxClaimable: maxClaimable || "0",
|
||||
price: price || undefined,
|
||||
}),
|
||||
),
|
||||
startTime: new Date(phase.startTime),
|
||||
}))
|
||||
.map((phase, idx) => {
|
||||
return {
|
||||
...phase,
|
||||
currencyAddress: phase.currencyAddress?.toLowerCase() || "0",
|
||||
currencyMetadata: {
|
||||
...phase.currencyMetadata,
|
||||
value: phase.currencyMetadata.value?.toString() || "0",
|
||||
},
|
||||
fromSdk: true,
|
||||
isEditing: false,
|
||||
maxClaimablePerWallet: phase.maxClaimablePerWallet?.toString() || "0",
|
||||
maxClaimableSupply: phase.maxClaimableSupply?.toString() || "0",
|
||||
merkleRootHash: (phase.merkleRootHash || "") as string,
|
||||
metadata: {
|
||||
...phase.metadata,
|
||||
name: phase?.metadata?.name || `Phase ${idx + 1}`,
|
||||
},
|
||||
price: phase.currencyMetadata.displayValue,
|
||||
snapshot: undefined, // Do not save snapshot in form data - its slow
|
||||
startTime: new Date(phase.startTime),
|
||||
};
|
||||
})
|
||||
.filter(
|
||||
(phase) => isMultiPhase || Number(phase.maxClaimableSupply) !== 0,
|
||||
);
|
||||
@@ -289,8 +325,19 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
name: "phases",
|
||||
});
|
||||
|
||||
const setPhaseSnapshot = (
|
||||
phaseIndex: number,
|
||||
snapshot: SnapshotEntry[] | undefined,
|
||||
) => {
|
||||
setPhaseSnapshots((prev) => ({
|
||||
...prev,
|
||||
[phaseIndex]: snapshot,
|
||||
}));
|
||||
};
|
||||
|
||||
const addPhase = (type: ClaimConditionType) => {
|
||||
const name = `${ClaimConditionTypeData[type].name} phase`;
|
||||
const newPhaseIndex = formFields.fields.length;
|
||||
|
||||
switch (type) {
|
||||
case "public":
|
||||
@@ -305,8 +352,9 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
...DEFAULT_PHASE,
|
||||
maxClaimablePerWallet: "0",
|
||||
metadata: { name },
|
||||
snapshot: [],
|
||||
snapshot: undefined,
|
||||
});
|
||||
setPhaseSnapshot(newPhaseIndex, []);
|
||||
break;
|
||||
|
||||
case "overrides":
|
||||
@@ -314,8 +362,9 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
...DEFAULT_PHASE,
|
||||
maxClaimablePerWallet: "1",
|
||||
metadata: { name },
|
||||
snapshot: [],
|
||||
snapshot: undefined,
|
||||
});
|
||||
setPhaseSnapshot(newPhaseIndex, []);
|
||||
break;
|
||||
|
||||
case "creator":
|
||||
@@ -325,16 +374,17 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
maxClaimableSupply: "unlimited",
|
||||
metadata: { name },
|
||||
price: "0",
|
||||
snapshot: walletAddress
|
||||
? [
|
||||
{
|
||||
address: walletAddress,
|
||||
maxClaimable: "unlimited",
|
||||
price: "0",
|
||||
},
|
||||
]
|
||||
: [],
|
||||
snapshot: undefined,
|
||||
});
|
||||
if (walletAddress) {
|
||||
setPhaseSnapshot(newPhaseIndex, [
|
||||
{
|
||||
address: walletAddress,
|
||||
maxClaimable: "unlimited",
|
||||
price: "0",
|
||||
},
|
||||
]);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -360,6 +410,12 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
);
|
||||
}
|
||||
try {
|
||||
// Merge snapshots back into phases for submission
|
||||
const phasesWithSnapshots = d.phases.map((phase, index) => ({
|
||||
...phase,
|
||||
snapshot: phaseSnapshots[index] || undefined,
|
||||
}));
|
||||
|
||||
const tx = setClaimPhasesTx(
|
||||
{
|
||||
contract,
|
||||
@@ -370,19 +426,11 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
? { type: "erc721" }
|
||||
: { tokenId: BigInt(tokenId || 0), type: "erc1155" }),
|
||||
},
|
||||
d.phases,
|
||||
phasesWithSnapshots,
|
||||
);
|
||||
await sendTx.mutateAsync(tx);
|
||||
|
||||
saveClaimPhaseNotification.onSuccess();
|
||||
|
||||
const newPhases = d.phases.map((phase) => ({
|
||||
...phase,
|
||||
fromSdk: true,
|
||||
isEditing: false,
|
||||
}));
|
||||
|
||||
form.setValue("phases", newPhases);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -446,17 +494,21 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
{!isAdmin && <p>Connect with admin wallet to edit claim conditions.</p>}
|
||||
|
||||
{controlledFields.map((field, index) => {
|
||||
const dropType: DropType = field.snapshot
|
||||
const snapshot = phaseSnapshots[index];
|
||||
const dropType: DropType = snapshot
|
||||
? field.maxClaimablePerWallet?.toString() === "0"
|
||||
? "specific"
|
||||
: "overrides"
|
||||
: "any";
|
||||
|
||||
const claimConditionType = getClaimConditionTypeFromPhase(field);
|
||||
const claimConditionType = getClaimConditionTypeFromPhase(
|
||||
field,
|
||||
snapshot,
|
||||
);
|
||||
|
||||
const isActive = activePhaseId === field.id;
|
||||
|
||||
const snapshotValue = field.snapshot?.map((v) =>
|
||||
const snapshotValue = snapshot?.map((v) =>
|
||||
typeof v === "string"
|
||||
? {
|
||||
address: v,
|
||||
@@ -482,9 +534,7 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
onClose={() => {
|
||||
setOpenSnapshotIndex(-1);
|
||||
}}
|
||||
setSnapshot={(snapshot) =>
|
||||
form.setValue(`phases.${index}.snapshot`, snapshot)
|
||||
}
|
||||
setSnapshot={(snapshot) => setPhaseSnapshot(index, snapshot)}
|
||||
value={snapshotValue}
|
||||
/>
|
||||
|
||||
@@ -503,6 +553,8 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
phaseIndex: index,
|
||||
setOpenSnapshotIndex,
|
||||
tokenDecimals: tokenDecimals.data,
|
||||
phaseSnapshots,
|
||||
setPhaseSnapshot,
|
||||
}}
|
||||
>
|
||||
<ClaimConditionsPhase
|
||||
@@ -510,6 +562,8 @@ export const ClaimConditionsForm: React.FC<ClaimConditionsFormProps> = ({
|
||||
isPending={sendTx.isPending}
|
||||
onRemove={() => {
|
||||
formFields.remove(index);
|
||||
// Clean up snapshot when phase is removed
|
||||
setPhaseSnapshot(index, undefined);
|
||||
}}
|
||||
/>
|
||||
</ClaimsConditionFormContext.Provider>
|
||||
|
||||
+4
-3
@@ -35,12 +35,15 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
|
||||
isActive,
|
||||
isMultiPhase,
|
||||
phaseIndex,
|
||||
phaseSnapshots,
|
||||
} = useClaimConditionsFormContext();
|
||||
|
||||
const toggleEditing = () => {
|
||||
form.setValue(`phases.${phaseIndex}.isEditing`, !field.isEditing);
|
||||
};
|
||||
|
||||
const snapshot = phaseSnapshots[phaseIndex];
|
||||
|
||||
return (
|
||||
<DynamicHeight transition="height 0.3s ease">
|
||||
<div className="border border-border rounded-lg bg-card relative">
|
||||
@@ -118,9 +121,7 @@ export const ClaimConditionsPhase: React.FC<ClaimConditionsPhaseProps> = ({
|
||||
<PhaseStartTimeInput />
|
||||
|
||||
<CreatorInput
|
||||
creatorAddress={
|
||||
(field.snapshot?.[0] as { address: string })?.address
|
||||
}
|
||||
creatorAddress={(snapshot?.[0] as { address: string })?.address}
|
||||
/>
|
||||
|
||||
<MaxClaimableSupplyInput />
|
||||
|
||||
+197
-106
@@ -4,12 +4,14 @@ import {
|
||||
CircleSlashIcon,
|
||||
RotateCcwIcon,
|
||||
} from "lucide-react";
|
||||
import { type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { useState } from "react";
|
||||
import { isAddress, type ThirdwebClient, ZERO_ADDRESS } from "thirdweb";
|
||||
import { DownloadableCode } from "@/components/blocks/code/downloadable-code";
|
||||
import { DropZone } from "@/components/blocks/drop-zone/drop-zone";
|
||||
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { InlineCode } from "@/components/ui/inline-code";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -27,24 +29,18 @@ import {
|
||||
} from "@/components/ui/table";
|
||||
import { ToolTipLabel } from "@/components/ui/tooltip";
|
||||
import { useCsvUpload } from "@/hooks/useCsvUpload";
|
||||
import type { SnapshotEntry } from "./legacy-zod-schema";
|
||||
|
||||
interface SnapshotAddressInput {
|
||||
address: string;
|
||||
maxClaimable?: string;
|
||||
price?: string;
|
||||
currencyAddress?: string;
|
||||
isValid?: boolean;
|
||||
}
|
||||
interface SnapshotUploadProps {
|
||||
setSnapshot: (snapshot: SnapshotAddressInput[]) => void;
|
||||
setSnapshot: (snapshot: (SnapshotEntry & { ensName?: string })[]) => void;
|
||||
dropType: "specific" | "any" | "overrides";
|
||||
isDisabled: boolean;
|
||||
value?: SnapshotAddressInput[] | undefined;
|
||||
value?: (SnapshotEntry & { ensName?: string })[] | undefined;
|
||||
onClose: () => void;
|
||||
client: ThirdwebClient;
|
||||
}
|
||||
|
||||
const csvParser = (items: SnapshotAddressInput[]): SnapshotAddressInput[] => {
|
||||
const csvParser = (items: SnapshotEntry[]): SnapshotEntry[] => {
|
||||
return items
|
||||
.map(({ address, maxClaimable, price, currencyAddress }) => ({
|
||||
address: (address || "").trim(),
|
||||
@@ -55,92 +51,125 @@ const csvParser = (items: SnapshotAddressInput[]): SnapshotAddressInput[] => {
|
||||
.filter(({ address }) => address !== "");
|
||||
};
|
||||
|
||||
function SnapshotDataTable({ data }: { data: SnapshotAddressInput[] }) {
|
||||
function SnapshotDataTable({
|
||||
data,
|
||||
}: {
|
||||
data: (SnapshotEntry & {
|
||||
ensName?: string;
|
||||
})[];
|
||||
}) {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const itemsPerPage = 10;
|
||||
|
||||
const totalPages = Math.ceil(data.length / itemsPerPage);
|
||||
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||
const endIndex = startIndex + itemsPerPage;
|
||||
const currentData = data.slice(startIndex, endIndex);
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableContainer>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Address</TableHead>
|
||||
<TableHead>Max claimable</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Currency Address</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data.map((item) => (
|
||||
<TableRow key={item.address}>
|
||||
<TableCell>
|
||||
{item.isValid ? (
|
||||
item.address
|
||||
) : (
|
||||
<ToolTipLabel
|
||||
label={
|
||||
item.address === ZERO_ADDRESS
|
||||
? "Cannot send tokens to ZERO_ADDRESS"
|
||||
: item.address.startsWith("0x")
|
||||
? "Address is not valid"
|
||||
: "Address couldn't be resolved"
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CircleAlertIcon className="size-4 text-red-500" />
|
||||
<div className="cursor-default font-bold text-red-500">
|
||||
{item.address}
|
||||
</div>
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.maxClaimable === "0" || !item.maxClaimable
|
||||
? "Default"
|
||||
: item.maxClaimable === "unlimited"
|
||||
? "Unlimited"
|
||||
: item.maxClaimable}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.price === "0"
|
||||
? "Free"
|
||||
: !item.price || item.price === "unlimited"
|
||||
? "Default"
|
||||
: item.price}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.currencyAddress ===
|
||||
"0x0000000000000000000000000000000000000000" ||
|
||||
!item.currencyAddress
|
||||
? "Default"
|
||||
: item.currencyAddress}
|
||||
</TableCell>
|
||||
<div className="border bg-card rounded-lg overflow-hidden">
|
||||
<TableContainer className="border-none rounded-none">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Address</TableHead>
|
||||
<TableHead>Max claimable</TableHead>
|
||||
<TableHead>Price</TableHead>
|
||||
<TableHead>Currency Address</TableHead>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{currentData.map((item) => (
|
||||
<TableRow key={item.address}>
|
||||
<TableCell>
|
||||
{isAddress(item.address) && item.address !== ZERO_ADDRESS ? (
|
||||
item.ensName || item.address
|
||||
) : (
|
||||
<ToolTipLabel
|
||||
label={
|
||||
item.address === ZERO_ADDRESS
|
||||
? "Cannot send tokens to ZERO_ADDRESS"
|
||||
: item.address.startsWith("0x")
|
||||
? "Address is not valid"
|
||||
: "Address couldn't be resolved"
|
||||
}
|
||||
>
|
||||
<div className="flex flex-row items-center gap-2">
|
||||
<CircleAlertIcon className="size-4 text-red-500" />
|
||||
<div className="cursor-default font-bold text-red-500">
|
||||
{item.ensName || item.address}
|
||||
</div>
|
||||
</div>
|
||||
</ToolTipLabel>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.maxClaimable === "0" || !item.maxClaimable
|
||||
? "Default"
|
||||
: item.maxClaimable === "unlimited"
|
||||
? "Unlimited"
|
||||
: item.maxClaimable}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.price === "0"
|
||||
? "Free"
|
||||
: !item.price || item.price === "unlimited"
|
||||
? "Default"
|
||||
: item.price}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{item.currencyAddress ===
|
||||
"0x0000000000000000000000000000000000000000" ||
|
||||
!item.currencyAddress
|
||||
? "Default"
|
||||
: item.currencyAddress}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="border-t p-4 lg:px-6">
|
||||
<PaginationButtons
|
||||
activePage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageClick={handlePageChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
const UploadSnapshot: React.FC<Omit<SnapshotUploadProps, "value">> = ({
|
||||
setSnapshot,
|
||||
dropType,
|
||||
isDisabled,
|
||||
value,
|
||||
onClose,
|
||||
client,
|
||||
}) => {
|
||||
const csvUpload = useCsvUpload<SnapshotAddressInput>({
|
||||
const csvUpload = useCsvUpload<SnapshotEntry>({
|
||||
client,
|
||||
csvParser,
|
||||
defaultRawData: value,
|
||||
});
|
||||
|
||||
const normalizeData = csvUpload.normalizeQuery.data;
|
||||
|
||||
if (!normalizeData) {
|
||||
return (
|
||||
<div className="flex min-h-[400px] w-full grow items-center justify-center">
|
||||
<div className="flex min-h-[400px] w-full flex-col grow items-center justify-center">
|
||||
<Spinner className="size-10" />
|
||||
<p className="text-base text-foreground mt-5">Resolving ENS</p>
|
||||
<p className="text-sm text-muted-foreground mt-2">
|
||||
{csvUpload.normalizeProgress.current} /{" "}
|
||||
{csvUpload.normalizeProgress.total}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -154,6 +183,7 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
currencyAddress: o.currencyAddress,
|
||||
maxClaimable: o.maxClaimable,
|
||||
price: o.price,
|
||||
ensName: o.address.startsWith("0x") ? undefined : o.address,
|
||||
})),
|
||||
);
|
||||
onClose();
|
||||
@@ -163,41 +193,58 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
<div className="space-y-6">
|
||||
{csvUpload.rawData.length > 0 ? (
|
||||
<div>
|
||||
<SnapshotDataTable data={csvUpload.normalizeQuery.data.result} />
|
||||
<div className="flex justify-end gap-3 border-t py-6 border-dashed">
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={() => {
|
||||
csvUpload.reset();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<RotateCcwIcon className="size-4" />
|
||||
Reset
|
||||
</Button>
|
||||
<SnapshotDataTable
|
||||
data={csvUpload.normalizeQuery.data.result.map((x) => ({
|
||||
address: x.resolvedAddress,
|
||||
ensName: x.address.startsWith("0x") ? undefined : x.address,
|
||||
maxClaimable: x.maxClaimable,
|
||||
currencyAddress: x.currencyAddress,
|
||||
price: x.price,
|
||||
}))}
|
||||
/>
|
||||
|
||||
{csvUpload.normalizeQuery.data?.invalidFound ? (
|
||||
<div className="py-6">
|
||||
{csvUpload.normalizeQuery.data.invalidFound && (
|
||||
<p className="text-sm text-destructive-text mb-3">
|
||||
Invalid addresses found, please remove from the list to continue
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex justify-start items-center gap-3">
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={() => {
|
||||
csvUpload.removeInvalid();
|
||||
csvUpload.reset();
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<CircleSlashIcon className="size-4" />
|
||||
Remove invalid
|
||||
<RotateCcwIcon className="size-4" />
|
||||
Reset
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={onSave}
|
||||
>
|
||||
Next
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{csvUpload.normalizeQuery.data?.invalidFound ? (
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={() => {
|
||||
csvUpload.removeInvalid();
|
||||
}}
|
||||
>
|
||||
<CircleSlashIcon className="size-4" />
|
||||
Remove invalid
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
className="gap-2"
|
||||
disabled={isDisabled || csvUpload.rawData.length === 0}
|
||||
onClick={onSave}
|
||||
>
|
||||
Next
|
||||
<ArrowRightIcon className="size-4" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -308,6 +355,23 @@ const SnapshotViewerSheetContent: React.FC<SnapshotUploadProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function ViewSnapshot(props: {
|
||||
data: (SnapshotEntry & { ensName?: string })[];
|
||||
onReset: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<SnapshotDataTable data={props.data} />
|
||||
<div className="py-6">
|
||||
<Button className="gap-2" onClick={props.onReset} variant="outline">
|
||||
<RotateCcwIcon className="size-4" />
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function SnapshotViewerSheet(
|
||||
props: SnapshotUploadProps & {
|
||||
isOpen: boolean;
|
||||
@@ -322,7 +386,7 @@ export function SnapshotViewerSheet(
|
||||
}}
|
||||
open={props.isOpen}
|
||||
>
|
||||
<SheetContent className="!w-full !max-w-4xl overflow-y-auto">
|
||||
<SheetContent className="!w-full !max-w-4xl overflow-y-auto flex flex-col">
|
||||
<SheetHeader className="mb-3">
|
||||
<SheetTitle className="text-left">Snapshot</SheetTitle>
|
||||
</SheetHeader>
|
||||
@@ -332,6 +396,33 @@ export function SnapshotViewerSheet(
|
||||
);
|
||||
}
|
||||
|
||||
function SnapshotViewerSheetContent(props: SnapshotUploadProps) {
|
||||
const [showEditMode, setShowEditMode] = useState(false);
|
||||
|
||||
if (showEditMode) {
|
||||
return (
|
||||
<UploadSnapshot
|
||||
setSnapshot={props.setSnapshot}
|
||||
dropType={props.dropType}
|
||||
isDisabled={props.isDisabled}
|
||||
onClose={props.onClose}
|
||||
client={props.client}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.value && props.value.length > 0) {
|
||||
return (
|
||||
<ViewSnapshot
|
||||
data={props.value || []}
|
||||
onReset={() => setShowEditMode(true)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <UploadSnapshot {...props} />;
|
||||
}
|
||||
|
||||
const snapshotWithMaxClaimable = `\
|
||||
address,maxClaimable
|
||||
0x0000000000000000000000000000000000000000,2
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
|
||||
export function LoadingPage() {
|
||||
return (
|
||||
|
||||
+1
-1
@@ -23,7 +23,7 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Spinner } from "@/components/ui/Spinner/Spinner";
|
||||
import { Spinner } from "@/components/ui/Spinner";
|
||||
import { UnderlineLink } from "@/components/ui/UnderlineLink";
|
||||
import { useAddContractToProject } from "@/hooks/project-contracts";
|
||||
import type { EVMContractInfo } from "@/hooks/useActiveChainId";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user