Skip to content

Commit 3673109

Browse files
committed
Dashboard: Migrate /tokens contract page from chakra to tailwind (#7695)
<!-- ## 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 focuses on improving the functionality and user experience of token management features in a dashboard application, including enhancements to file downloads, airdrop processes, and form validations. ### Detailed summary - Removed unused `supportedERCs` from `ContractTokensPage`. - Added redirects for unsupported ERC20 tokens. - Improved the `LazyMintNftForm` with validation on image uploads. - Introduced `DownloadableCode` component for code downloads. - Added `AirdropCSVTable` for better CSV data handling. - Enhanced `TokenAirdropForm` with improved transaction handling and notifications. - Refactored various token management buttons to use consistent form structures and validation. - Updated airdrop upload process to handle CSV files more effectively with user feedback. > ✨ 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 new DownloadableCode component for displaying and downloading code examples. * Added a paginated CSV table for airdrop address and quantity previews. * **Refactor** * Simplified and unified token action forms (airdrop, burn, claim, mint, transfer) with improved validation, streamlined UI, and consistent notifications. * Replaced custom code example components with the new DownloadableCode component across relevant views. * Updated CSV upload experience with clearer instructions and reusable components. * Improved conditional navigation for unsupported token standards. * **Bug Fixes** * Enhanced form validation and error handling for token-related actions. * **Chores** * Updated import paths for consistency and maintainability. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 1607229 commit 3673109

File tree

18 files changed

+795
-851
lines changed

18 files changed

+795
-851
lines changed

apps/dashboard/src/@/components/batch-upload/upload-step.tsx

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import { ArrowDownToLineIcon } from "lucide-react";
21
import { useState } from "react";
3-
import { Button } from "@/components/ui/button";
4-
import { CodeClient } from "@/components/ui/code/code.client";
52
import { InlineCode } from "@/components/ui/inline-code";
63
import { TabButtons } from "@/components/ui/tabs";
7-
import { handleDownload } from "../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/download-file-button";
4+
import { DownloadableCode } from "../blocks/code/downloadable-code";
85
import { DropZone } from "../blocks/drop-zone/drop-zone";
96

107
interface UploadStepProps {
@@ -105,7 +102,7 @@ export function UploadStep(props: UploadStepProps) {
105102
"All other columns will be treated as Attributes. For example: See 'eyes', 'nose' below."}
106103
</p>
107104

108-
<ExampleCode
105+
<DownloadableCode
109106
code={tab === "csv" ? csv_example_basic : json_example_basic}
110107
fileNameWithExtension={
111108
tab === "csv" ? "example.csv" : "example.json"
@@ -120,7 +117,7 @@ export function UploadStep(props: UploadStepProps) {
120117
name of your files to the <InlineCode code="image" /> and{" "}
121118
<InlineCode code="animation_url" />{" "}
122119
{tab === "csv" ? "columns" : "properties"}.{" "}
123-
<ExampleCode
120+
<DownloadableCode
124121
code={
125122
tab === "csv"
126123
? csv_with_image_number_example
@@ -146,7 +143,7 @@ export function UploadStep(props: UploadStepProps) {
146143
{tab === "csv" ? "columns" : "properties"}. instead of uploading
147144
the assets and just upload a single{" "}
148145
{tab === "csv" ? "CSV" : "JSON"} file.
149-
<ExampleCode
146+
<DownloadableCode
150147
code={
151148
tab === "csv"
152149
? csv_with_image_link_example
@@ -180,37 +177,6 @@ function Section(props: { title: string; children: React.ReactNode }) {
180177
);
181178
}
182179

183-
function ExampleCode(props: {
184-
code: string;
185-
lang: "csv" | "json";
186-
fileNameWithExtension: string;
187-
}) {
188-
return (
189-
<div className="!my-3 relative">
190-
<CodeClient
191-
code={props.code}
192-
lang={props.lang}
193-
scrollableClassName="max-h-[300px] bg-card"
194-
/>
195-
196-
<Button
197-
className="absolute top-3.5 right-14 mt-[1px] h-auto bg-background p-2"
198-
onClick={() => {
199-
handleDownload({
200-
fileContent: props.code,
201-
fileFormat: props.lang === "csv" ? "text/csv" : "application/json",
202-
fileNameWithExtension: props.fileNameWithExtension,
203-
});
204-
}}
205-
size="sm"
206-
variant="outline"
207-
>
208-
<ArrowDownToLineIcon className="size-3" />
209-
</Button>
210-
</div>
211-
);
212-
}
213-
214180
const csv_example_basic = `\
215181
name,description,background_color,eyes,nose
216182
Token 0 Name,Token 0 Description,#0098EE,red,green
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"use client";
2+
import { ArrowDownToLineIcon } from "lucide-react";
3+
import { Button } from "@/components/ui/button";
4+
import { CodeClient } from "@/components/ui/code/code.client";
5+
import { handleDownload } from "../download-file-button";
6+
7+
export function DownloadableCode(props: {
8+
code: string;
9+
lang: "csv" | "json";
10+
fileNameWithExtension: string;
11+
}) {
12+
return (
13+
<div className="!my-3 relative">
14+
<CodeClient
15+
code={props.code}
16+
lang={props.lang}
17+
scrollableClassName="max-h-[300px] bg-background"
18+
/>
19+
20+
<Button
21+
className="absolute top-3.5 right-14 mt-[1px] h-auto bg-background p-2"
22+
onClick={() => {
23+
handleDownload({
24+
fileContent: props.code,
25+
fileFormat: props.lang === "csv" ? "text/csv" : "application/json",
26+
fileNameWithExtension: props.fileNameWithExtension,
27+
});
28+
}}
29+
size="sm"
30+
variant="outline"
31+
>
32+
<ArrowDownToLineIcon className="size-3" />
33+
</Button>
34+
</div>
35+
);
36+
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/[tokenId]/components/airdrop-tab.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,6 @@ const AirdropTab: React.FC<AirdropTabProps> = ({
110110
</SheetHeader>
111111
<AirdropUpload
112112
client={contract.client}
113-
onClose={() => setOpen(false)}
114113
setAirdrop={(value) =>
115114
setValue("addresses", value, { shouldDirty: true })
116115
}

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/nfts/components/lazy-mint-form.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ export function LazyMintNftForm({
160160
<FormField
161161
control={control}
162162
name="image"
163-
render={({ field }) => (
163+
render={() => (
164164
<FormItem>
165165
<FormLabel>Cover Image</FormLabel>
166166
<FormControl>
@@ -169,7 +169,11 @@ export function LazyMintNftForm({
169169
className="rounded-lg bg-card border border-border transition-all"
170170
client={contract.client}
171171
previewMaxWidth="200px"
172-
setValue={(file) => setValue("image", file)}
172+
setValue={(file) =>
173+
setValue("image", file, {
174+
shouldValidate: true,
175+
})
176+
}
173177
showUploadButton
174178
value={image}
175179
/>

apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/ContractTokensPage.client.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,12 @@ export function ContractTokensPageClient(props: {
2222
return <ErrorPage />;
2323
}
2424

25-
const { supportedERCs, functionSelectors } = metadataQuery.data;
25+
const { functionSelectors } = metadataQuery.data;
2626

2727
return (
2828
<ContractTokensPage
2929
contract={props.contract}
3030
isClaimToSupported={isClaimToSupported(functionSelectors)}
31-
isERC20={supportedERCs.isERC20}
3231
isLoggedIn={props.isLoggedIn}
3332
isMintToSupported={isMintToSupported(functionSelectors)}
3433
/>
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
"use client";
2-
import { LinkButton } from "chakra/button";
3-
import { Card } from "chakra/card";
4-
import { Heading } from "chakra/heading";
5-
import { Text } from "chakra/text";
2+
63
import type { ThirdwebContract } from "thirdweb";
74
import { TokenAirdropButton } from "./components/airdrop-button";
85
import { TokenBurnButton } from "./components/burn-button";
@@ -13,41 +10,17 @@ import { TokenTransferButton } from "./components/transfer-button";
1310

1411
interface ContractTokenPageProps {
1512
contract: ThirdwebContract;
16-
isERC20: boolean;
1713
isMintToSupported: boolean;
1814
isClaimToSupported: boolean;
1915
isLoggedIn: boolean;
2016
}
2117

22-
export const ContractTokensPage: React.FC<ContractTokenPageProps> = ({
18+
export function ContractTokensPage({
2319
contract,
24-
isERC20,
2520
isMintToSupported,
2621
isClaimToSupported,
2722
isLoggedIn,
28-
}) => {
29-
if (!isERC20) {
30-
return (
31-
<Card className="flex flex-col gap-3">
32-
{/* TODO extract this out into it's own component and make it better */}
33-
<Heading size="subtitle.md">No Token extension enabled</Heading>
34-
<Text>
35-
To enable Token features you will have to extend an ERC20 interface in
36-
your contract.
37-
</Text>
38-
<div>
39-
<LinkButton
40-
colorScheme="purple"
41-
href="https://portal.thirdweb.com/contracts/build/extensions/erc-20/ERC20"
42-
isExternal
43-
>
44-
Learn more
45-
</LinkButton>
46-
</div>
47-
</Card>
48-
);
49-
}
50-
23+
}: ContractTokenPageProps) {
5124
return (
5225
<div className="flex flex-col gap-6">
5326
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
@@ -68,4 +41,4 @@ export const ContractTokensPage: React.FC<ContractTokenPageProps> = ({
6841
<TokenDetailsCard contract={contract} />
6942
</div>
7043
);
71-
};
44+
}
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
"use client";
22

33
import { DropletIcon } from "lucide-react";
4-
import { useState } from "react";
54
import type { ThirdwebContract } from "thirdweb";
6-
import { balanceOf } from "thirdweb/extensions/erc20";
7-
import { useActiveAccount, useReadContract } from "thirdweb/react";
85
import { Button } from "@/components/ui/button";
96
import {
107
Sheet,
@@ -15,42 +12,26 @@ import {
1512
} from "@/components/ui/sheet";
1613
import { TokenAirdropForm } from "./airdrop-form";
1714

18-
interface TokenAirdropButtonProps {
15+
export function TokenAirdropButton(props: {
1916
contract: ThirdwebContract;
2017
isLoggedIn: boolean;
21-
}
22-
23-
export const TokenAirdropButton: React.FC<TokenAirdropButtonProps> = ({
24-
contract,
25-
isLoggedIn,
26-
...restButtonProps
27-
}) => {
28-
const address = useActiveAccount()?.address;
29-
const tokenBalanceQuery = useReadContract(balanceOf, {
30-
address: address || "",
31-
contract,
32-
queryOptions: { enabled: !!address },
33-
});
34-
const hasBalance = tokenBalanceQuery.data && tokenBalanceQuery.data > 0n;
35-
const [open, setOpen] = useState(false);
18+
}) {
3619
return (
37-
<Sheet onOpenChange={setOpen} open={open}>
20+
<Sheet>
3821
<SheetTrigger asChild>
39-
<Button
40-
variant="primary"
41-
{...restButtonProps}
42-
className="gap-2"
43-
disabled={!hasBalance}
44-
>
45-
<DropletIcon size={16} /> Airdrop
22+
<Button className="gap-2">
23+
<DropletIcon className="size-4" /> Airdrop
4624
</Button>
4725
</SheetTrigger>
48-
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
49-
<SheetHeader>
26+
<SheetContent className="!w-full lg:!max-w-3xl flex flex-col gap-0">
27+
<SheetHeader className="mb-4">
5028
<SheetTitle className="text-left">Airdrop tokens</SheetTitle>
5129
</SheetHeader>
52-
<TokenAirdropForm contract={contract} isLoggedIn={isLoggedIn} />
30+
<TokenAirdropForm
31+
contract={props.contract}
32+
isLoggedIn={props.isLoggedIn}
33+
/>
5334
</SheetContent>
5435
</Sheet>
5536
);
56-
};
37+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import { AlertCircleIcon } from "lucide-react";
2+
import { useState } from "react";
3+
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
4+
import {
5+
Table,
6+
TableBody,
7+
TableCell,
8+
TableContainer,
9+
TableHead,
10+
TableHeader,
11+
TableRow,
12+
} from "@/components/ui/table";
13+
import { cn } from "@/lib/utils";
14+
15+
export function AirdropCSVTable(props: {
16+
data: { address: string; quantity: string; isValid?: boolean }[];
17+
}) {
18+
const pageSize = 10;
19+
const [page, setPage] = useState(1);
20+
const totalPages = Math.ceil(props.data.length / pageSize);
21+
const paginatedData = props.data.slice(
22+
(page - 1) * pageSize,
23+
page * pageSize,
24+
);
25+
26+
return (
27+
<div className="border rounded-lg">
28+
<TableContainer className="border-0">
29+
<Table>
30+
<TableHeader>
31+
<TableRow>
32+
<TableHead>Address</TableHead>
33+
<TableHead>Quantity</TableHead>
34+
</TableRow>
35+
</TableHeader>
36+
<TableBody>
37+
{paginatedData.map((row) => (
38+
<TableRow key={row.address}>
39+
<TableCell>
40+
<div
41+
className={cn(
42+
"flex items-center gap-2",
43+
!row.isValid && "text-red-500",
44+
)}
45+
>
46+
{row.address}
47+
{!row.isValid && (
48+
<AlertCircleIcon className="size-4 text-red-500" />
49+
)}
50+
</div>
51+
</TableCell>
52+
<TableCell>{row.quantity}</TableCell>
53+
</TableRow>
54+
))}
55+
</TableBody>
56+
</Table>
57+
</TableContainer>
58+
{totalPages > 1 && (
59+
<div className="border-t py-4">
60+
<PaginationButtons
61+
activePage={page}
62+
totalPages={totalPages}
63+
onPageClick={setPage}
64+
/>
65+
</div>
66+
)}
67+
</div>
68+
);
69+
}

0 commit comments

Comments
 (0)