Skip to content

Commit 73629e9

Browse files
committed
[TOOL-4531] Dashboard: Add Token Asset creation wizard (#7081)
<!-- ## title your PR with this format: "[SDK/Dashboard/Portal] Feature/Fix: Concise title for the changes" If you did not copy the branch name from Linear, paste the issue tag here (format is TEAM-0000): ## Notes for the reviewer Anything important to call out? Be sure to also clarify these in your comments. ## How to test Unit tests, playground, etc. --> <!-- start pr-codex --> --- ## PR-Codex overview This PR introduces several enhancements and features related to asset management, including deployment tracking, UI updates, and new forms for creating and managing tokens. It also refines existing components and adds new functionalities for improved user experience. ### Detailed summary - Added `deploymentType` and `contractType` properties in various components. - Introduced `revalidatePathAction` for path revalidation. - Updated UI components for better asset management. - Enhanced forms for token creation with validation. - Implemented multi-step status tracking for token launch processes. - Improved error handling and user feedback in forms. - Added new charts for token distribution visualization. > The following files were skipped due to too many changes: `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/launch/launch-token.tsx`, `apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/assets/create/distribution/token-airdrop.tsx` > ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}` <!-- end pr-codex --> <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Introduced a comprehensive asset management interface for creating, importing, and managing ERC-20 tokens within team and project dashboards. - Added a multi-step token creation flow with form validation, including token info, distribution, sale settings, airdrop CSV upload, and launch process with real-time status tracking. - Implemented visual token allocation charts and multi-step progress indicators for enhanced user experience. - Added a dedicated "Assets" section in the sidebar with a "New" badge. - Added a horizontal segmented distribution bar chart component for visualizing token allocations. - Introduced a multi-step status component to track asynchronous step executions with retry capabilities. - Added token airdrop CSV upload and validation feature with user-friendly UI and error handling. - Added token sale configuration section with price and allocation inputs. - Enhanced import modal to support both contract and asset imports with contextual UI and behavior. - Added new pages and components to support token asset creation and management workflows, including cards for asset actions. - Added server-side path revalidation support for pages and layouts. - **Improvements** - Enhanced contract and asset import workflows with clearer UI, context-aware dialogs, and improved contract table variant handling. - Updated number input fields to remove browser-native spinners for a cleaner appearance. - Improved analytics event tracking for user interactions and deployment steps. - Refined UI components for consistent styling and accessibility. - Fixed sidebar badge placement and updated UI text for clarity. - Updated dialogs and payment embeds to reference transaction chain instead of active wallet chain and set default payment amounts. - Changed token supply label to "Circulating Supply" for clarity. - Replaced Chakra UI spinner and styled elements with local components for simplified UI. - **Bug Fixes** - Corrected UI typos and improved analytics event tracking for deployment errors. - **Documentation** - Added Storybook stories for new components to facilitate UI testing and documentation. - **Chores** - Refactored and extended types and props to support new asset and contract features, ensuring future extensibility. - Added tracking utilities for token deployment and asset creation analytics. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 23029db commit 73629e9

File tree

36 files changed

+2717
-78
lines changed

36 files changed

+2717
-78
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"use server";
2+
3+
import { revalidatePath } from "next/cache";
4+
5+
export async function revalidatePathAction(
6+
path: string,
7+
type: "page" | "layout",
8+
) {
9+
revalidatePath(path, type);
10+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { cn } from "@/lib/utils";
2+
3+
export type Segment = {
4+
label: string;
5+
percent: number;
6+
color: string;
7+
};
8+
9+
type DistributionBarChartProps = {
10+
segments: Segment[];
11+
title: string;
12+
};
13+
export function DistributionBarChart(props: DistributionBarChartProps) {
14+
const totalPercentage = props.segments.reduce(
15+
(sum, segment) => sum + segment.percent,
16+
0,
17+
);
18+
19+
const invalidTotalPercentage = totalPercentage !== 100;
20+
21+
return (
22+
<div>
23+
<div className="mb-2 flex items-center justify-between">
24+
<h3 className="font-medium text-sm">{props.title}</h3>
25+
<div
26+
className={cn(
27+
"font-medium text-muted-foreground text-sm",
28+
invalidTotalPercentage && "text-red-500",
29+
)}
30+
>
31+
Total: {totalPercentage}%
32+
</div>
33+
</div>
34+
35+
{/* Bar */}
36+
<div className="flex h-3 overflow-hidden rounded-lg">
37+
{props.segments.map((segment) => {
38+
return (
39+
<div
40+
key={segment.label}
41+
className="flex h-full items-center justify-center transition-all duration-200"
42+
style={{
43+
width: `${segment.percent}%`,
44+
backgroundColor: segment.color,
45+
}}
46+
/>
47+
);
48+
})}
49+
</div>
50+
51+
{/* Legends */}
52+
<div className="mt-3 flex flex-col gap-1 lg:flex-row lg:gap-6">
53+
{props.segments.map((segment) => {
54+
return (
55+
<div key={segment.label} className="flex items-center gap-1.5">
56+
<div
57+
className="size-3 rounded-full"
58+
style={{
59+
backgroundColor: segment.color,
60+
}}
61+
/>
62+
<p className="text-sm">
63+
{segment.label}: {segment.percent}%
64+
</p>
65+
</div>
66+
);
67+
})}
68+
</div>
69+
</div>
70+
);
71+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import type { Meta, StoryObj } from "@storybook/react";
2+
import { MultiStepStatus } from "./multi-step-status";
3+
4+
const meta = {
5+
title: "Blocks/MultiStepStatus",
6+
component: MultiStepStatus,
7+
decorators: [
8+
(Story) => (
9+
<div className="container w-full max-w-md py-10">
10+
<Story />
11+
</div>
12+
),
13+
],
14+
} satisfies Meta<typeof MultiStepStatus>;
15+
16+
export default meta;
17+
type Story = StoryObj<typeof meta>;
18+
19+
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
20+
21+
export const AllStates: Story = {
22+
args: {
23+
steps: [
24+
{
25+
status: "completed",
26+
label: "Connect Wallet",
27+
retryLabel: "Failed to connect wallet",
28+
execute: async () => {
29+
await sleep(1000);
30+
},
31+
},
32+
{
33+
status: "pending",
34+
label: "Sign Message",
35+
retryLabel: "Failed to sign message",
36+
execute: async () => {
37+
await sleep(1000);
38+
},
39+
},
40+
{
41+
status: "error",
42+
label: "Approve Transaction",
43+
retryLabel: "Transaction approval failed",
44+
execute: async () => {
45+
await sleep(1000);
46+
},
47+
},
48+
{
49+
status: "idle",
50+
label: "Confirm Transaction",
51+
retryLabel: "Transaction confirmation failed",
52+
execute: async () => {
53+
await sleep(1000);
54+
},
55+
},
56+
{
57+
status: "idle",
58+
label: "Finalize",
59+
retryLabel: "Finalization failed",
60+
execute: async () => {
61+
await sleep(1000);
62+
},
63+
},
64+
],
65+
},
66+
};
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
"use client";
2+
3+
import { Button } from "@/components/ui/button";
4+
import {
5+
AlertCircleIcon,
6+
CircleCheckIcon,
7+
CircleIcon,
8+
RefreshCwIcon,
9+
} from "lucide-react";
10+
import { DynamicHeight } from "../../ui/DynamicHeight";
11+
import { Spinner } from "../../ui/Spinner/Spinner";
12+
13+
export type MultiStepState = {
14+
status: "idle" | "pending" | "completed" | "error";
15+
retryLabel: string;
16+
label: string;
17+
execute: () => Promise<void>;
18+
};
19+
20+
export function MultiStepStatus(props: {
21+
steps: MultiStepState[];
22+
}) {
23+
return (
24+
<DynamicHeight>
25+
<div className="space-y-4">
26+
{props.steps.map((step) => (
27+
<div key={step.label} className="flex items-start space-x-3 ">
28+
{step.status === "completed" ? (
29+
<CircleCheckIcon className="mt-0.5 size-5 flex-shrink-0 text-green-500" />
30+
) : step.status === "pending" ? (
31+
<Spinner className="mt-0.5 size-5 flex-shrink-0 text-foreground" />
32+
) : step.status === "error" ? (
33+
<AlertCircleIcon className="mt-0.5 size-5 flex-shrink-0 text-red-500" />
34+
) : (
35+
<CircleIcon className="mt-0.5 size-5 flex-shrink-0 text-muted-foreground/70" />
36+
)}
37+
<div className="flex-1">
38+
<p
39+
className={`font-medium ${
40+
step.status === "pending"
41+
? "text-foreground"
42+
: step.status === "completed"
43+
? "text-green-500"
44+
: step.status === "error"
45+
? "text-red-500"
46+
: "text-muted-foreground/70"
47+
}`}
48+
>
49+
{step.label}
50+
</p>
51+
52+
{step.status === "error" && (
53+
<div className="mt-1 space-y-2">
54+
<p className="mb-1 text-red-500 text-sm">{step.retryLabel}</p>
55+
<Button
56+
variant="destructive"
57+
size="sm"
58+
className="gap-2"
59+
onClick={() => step.execute()}
60+
>
61+
<RefreshCwIcon className="size-4" />
62+
Retry
63+
</Button>
64+
</div>
65+
)}
66+
</div>
67+
</div>
68+
))}
69+
</div>
70+
</DynamicHeight>
71+
);
72+
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_components/claim-conditions/claim-conditions-form/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,10 @@ const getClaimConditionTypeFromPhase = (
126126
if (!phase.snapshot) {
127127
return "public";
128128
}
129+
129130
if (phase.snapshot) {
130131
if (
132+
phase.price === "0" &&
131133
typeof phase.snapshot !== "string" &&
132134
phase.snapshot.length === 1 &&
133135
phase.snapshot.some(

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/_layout/primary-dashboard-button.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ function AddToProjectModalContent(props: {
147147
teamId: params.teamId,
148148
projectId: params.projectId,
149149
chainId: props.chainId,
150+
deploymentType: undefined,
151+
contractType: undefined,
150152
},
151153
{
152154
onSuccess: () => {

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/supply-layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export function TokenDetailsCardUI(props: {
1414
</h2>
1515
<div className="flex flex-col gap-5 p-6 lg:flex-row">
1616
<Stat
17-
label="Total Supply"
17+
label="Circulating Supply"
1818
isPending={!tokenSupply}
1919
value={
2020
tokenSupply

apps/dashboard/src/app/(app)/account/contracts/DeployedContractsPageHeader.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export function DeployedContractsPageHeader(props: {
2626
}}
2727
teamId={props.teamId}
2828
projectId={props.projectId}
29+
type="contract"
2930
/>
3031

3132
<div className="container flex max-w-7xl flex-col gap-3 py-10 lg:flex-row lg:items-center lg:justify-between">

apps/dashboard/src/app/(app)/account/contracts/_components/DeployViaCLIOrImportCard.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export function DeployViaCLIOrImportCard(props: {
1919
return (
2020
<div className="rounded-lg border bg-card p-4 lg:p-6">
2121
<ImportModal
22+
type="contract"
2223
client={client}
2324
isOpen={importModalOpen}
2425
onClose={() => {

apps/dashboard/src/app/(app)/account/contracts/_components/DeployedContractsPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ async function DeployedContractsPageAsync(props: {
4444
teamId: props.teamId,
4545
projectId: props.projectId,
4646
authToken: props.authToken,
47+
deploymentType: undefined,
4748
});
4849

4950
return (
5051
<ClientOnly ssr={<Loading />}>
5152
<ContractTable
53+
variant="contract"
5254
contracts={deployedContracts}
5355
pageSize={10}
5456
teamId={props.teamId}

apps/dashboard/src/app/(app)/account/contracts/_components/getProjectContracts.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,28 @@ export type ProjectContract = {
77
chainId: string;
88
createdAt: string;
99
updatedAt: string;
10+
deploymentType: string | null;
11+
contractType: string | null;
1012
};
1113

1214
export async function getProjectContracts(options: {
1315
teamId: string;
1416
projectId: string;
1517
authToken: string;
18+
deploymentType: string | undefined;
1619
}) {
17-
const res = await fetch(
20+
const url = new URL(
1821
`${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${options.teamId}/projects/${options.projectId}/contracts`,
19-
{
20-
headers: {
21-
Authorization: `Bearer ${options.authToken}`,
22-
},
23-
},
2422
);
23+
if (options.deploymentType) {
24+
url.searchParams.set("deploymentType", options.deploymentType);
25+
}
26+
27+
const res = await fetch(url, {
28+
headers: {
29+
Authorization: `Bearer ${options.authToken}`,
30+
},
31+
});
2532

2633
if (!res.ok) {
2734
const errorMessage = await res.text();

apps/dashboard/src/app/(app)/account/contracts/_components/getSortedDeployedContracts.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,13 @@ export async function getSortedDeployedContracts(params: {
99
teamId: string;
1010
projectId: string;
1111
authToken: string;
12+
deploymentType: string | undefined;
1213
}) {
1314
const contracts = await getProjectContracts({
1415
teamId: params.teamId,
1516
projectId: params.projectId,
1617
authToken: params.authToken,
18+
deploymentType: params.deploymentType,
1719
});
1820

1921
const chainIds = Array.from(new Set(contracts.map((c) => c.chainId)));

0 commit comments

Comments
 (0)