Skip to content

Dashboard: Migrate /tokens contract page from chakra to tailwind #7695

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 4 additions & 38 deletions apps/dashboard/src/@/components/batch-upload/upload-step.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { ArrowDownToLineIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { CodeClient } from "@/components/ui/code/code.client";
import { InlineCode } from "@/components/ui/inline-code";
import { TabButtons } from "@/components/ui/tabs";
import { handleDownload } from "../../../app/(app)/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/download-file-button";
import { DownloadableCode } from "../blocks/code/downloadable-code";
import { DropZone } from "../blocks/drop-zone/drop-zone";

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

<ExampleCode
<DownloadableCode
code={tab === "csv" ? csv_example_basic : json_example_basic}
fileNameWithExtension={
tab === "csv" ? "example.csv" : "example.json"
Expand All @@ -120,7 +117,7 @@ export function UploadStep(props: UploadStepProps) {
name of your files to the <InlineCode code="image" /> and{" "}
<InlineCode code="animation_url" />{" "}
{tab === "csv" ? "columns" : "properties"}.{" "}
<ExampleCode
<DownloadableCode
code={
tab === "csv"
? csv_with_image_number_example
Expand All @@ -146,7 +143,7 @@ export function UploadStep(props: UploadStepProps) {
{tab === "csv" ? "columns" : "properties"}. instead of uploading
the assets and just upload a single{" "}
{tab === "csv" ? "CSV" : "JSON"} file.
<ExampleCode
<DownloadableCode
code={
tab === "csv"
? csv_with_image_link_example
Expand Down Expand Up @@ -180,37 +177,6 @@ function Section(props: { title: string; children: React.ReactNode }) {
);
}

function ExampleCode(props: {
code: string;
lang: "csv" | "json";
fileNameWithExtension: string;
}) {
return (
<div className="!my-3 relative">
<CodeClient
code={props.code}
lang={props.lang}
scrollableClassName="max-h-[300px] bg-card"
/>

<Button
className="absolute top-3.5 right-14 mt-[1px] h-auto bg-background p-2"
onClick={() => {
handleDownload({
fileContent: props.code,
fileFormat: props.lang === "csv" ? "text/csv" : "application/json",
fileNameWithExtension: props.fileNameWithExtension,
});
}}
size="sm"
variant="outline"
>
<ArrowDownToLineIcon className="size-3" />
</Button>
</div>
);
}

const csv_example_basic = `\
name,description,background_color,eyes,nose
Token 0 Name,Token 0 Description,#0098EE,red,green
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";
import { ArrowDownToLineIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { CodeClient } from "@/components/ui/code/code.client";
import { handleDownload } from "../download-file-button";

export function DownloadableCode(props: {
code: string;
lang: "csv" | "json";
fileNameWithExtension: string;
}) {
return (
<div className="!my-3 relative">
<CodeClient
code={props.code}
lang={props.lang}
scrollableClassName="max-h-[300px] bg-background"
/>

<Button
className="absolute top-3.5 right-14 mt-[1px] h-auto bg-background p-2"
onClick={() => {
handleDownload({
fileContent: props.code,
fileFormat: props.lang === "csv" ? "text/csv" : "application/json",
fileNameWithExtension: props.fileNameWithExtension,
});
}}
size="sm"
variant="outline"
>
<ArrowDownToLineIcon className="size-3" />
</Button>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ const AirdropTab: React.FC<AirdropTabProps> = ({
</SheetHeader>
<AirdropUpload
client={contract.client}
onClose={() => setOpen(false)}
setAirdrop={(value) =>
setValue("addresses", value, { shouldDirty: true })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ export function LazyMintNftForm({
<FormField
control={control}
name="image"
render={({ field }) => (
render={() => (
<FormItem>
<FormLabel>Cover Image</FormLabel>
<FormControl>
Expand All @@ -169,7 +169,11 @@ export function LazyMintNftForm({
className="rounded-lg bg-card border border-border transition-all"
client={contract.client}
previewMaxWidth="200px"
setValue={(file) => setValue("image", file)}
setValue={(file) =>
setValue("image", file, {
shouldValidate: true,
})
}
showUploadButton
value={image}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,12 @@ export function ContractTokensPageClient(props: {
return <ErrorPage />;
}

const { supportedERCs, functionSelectors } = metadataQuery.data;
const { functionSelectors } = metadataQuery.data;

return (
<ContractTokensPage
contract={props.contract}
isClaimToSupported={isClaimToSupported(functionSelectors)}
isERC20={supportedERCs.isERC20}
isLoggedIn={props.isLoggedIn}
isMintToSupported={isMintToSupported(functionSelectors)}
/>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
"use client";
import { LinkButton } from "chakra/button";
import { Card } from "chakra/card";
import { Heading } from "chakra/heading";
import { Text } from "chakra/text";

import type { ThirdwebContract } from "thirdweb";
import { TokenAirdropButton } from "./components/airdrop-button";
import { TokenBurnButton } from "./components/burn-button";
Expand All @@ -13,41 +10,17 @@ import { TokenTransferButton } from "./components/transfer-button";

interface ContractTokenPageProps {
contract: ThirdwebContract;
isERC20: boolean;
isMintToSupported: boolean;
isClaimToSupported: boolean;
isLoggedIn: boolean;
}

export const ContractTokensPage: React.FC<ContractTokenPageProps> = ({
export function ContractTokensPage({
contract,
isERC20,
isMintToSupported,
isClaimToSupported,
isLoggedIn,
}) => {
if (!isERC20) {
return (
<Card className="flex flex-col gap-3">
{/* TODO extract this out into it's own component and make it better */}
<Heading size="subtitle.md">No Token extension enabled</Heading>
<Text>
To enable Token features you will have to extend an ERC20 interface in
your contract.
</Text>
<div>
<LinkButton
colorScheme="purple"
href="https://portal.thirdweb.com/contracts/build/extensions/erc-20/ERC20"
isExternal
>
Learn more
</LinkButton>
</div>
</Card>
);
}

}: ContractTokenPageProps) {
return (
<div className="flex flex-col gap-6">
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
Expand All @@ -68,4 +41,4 @@ export const ContractTokensPage: React.FC<ContractTokenPageProps> = ({
<TokenDetailsCard contract={contract} />
</div>
);
};
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
"use client";

import { DropletIcon } from "lucide-react";
import { useState } from "react";
import type { ThirdwebContract } from "thirdweb";
import { balanceOf } from "thirdweb/extensions/erc20";
import { useActiveAccount, useReadContract } from "thirdweb/react";
import { Button } from "@/components/ui/button";
import {
Sheet,
Expand All @@ -15,42 +12,26 @@ import {
} from "@/components/ui/sheet";
import { TokenAirdropForm } from "./airdrop-form";

interface TokenAirdropButtonProps {
export function TokenAirdropButton(props: {
contract: ThirdwebContract;
isLoggedIn: boolean;
}

export const TokenAirdropButton: React.FC<TokenAirdropButtonProps> = ({
contract,
isLoggedIn,
...restButtonProps
}) => {
const address = useActiveAccount()?.address;
const tokenBalanceQuery = useReadContract(balanceOf, {
address: address || "",
contract,
queryOptions: { enabled: !!address },
});
const hasBalance = tokenBalanceQuery.data && tokenBalanceQuery.data > 0n;
const [open, setOpen] = useState(false);
}) {
return (
<Sheet onOpenChange={setOpen} open={open}>
<Sheet>
<SheetTrigger asChild>
<Button
variant="primary"
{...restButtonProps}
className="gap-2"
disabled={!hasBalance}
>
<DropletIcon size={16} /> Airdrop
<Button className="gap-2">
<DropletIcon className="size-4" /> Airdrop
</Button>
</SheetTrigger>
<SheetContent className="w-full overflow-y-auto sm:min-w-[540px] lg:min-w-[700px]">
<SheetHeader>
<SheetContent className="!w-full lg:!max-w-3xl flex flex-col gap-0">
<SheetHeader className="mb-4">
<SheetTitle className="text-left">Airdrop tokens</SheetTitle>
</SheetHeader>
<TokenAirdropForm contract={contract} isLoggedIn={isLoggedIn} />
<TokenAirdropForm
contract={props.contract}
isLoggedIn={props.isLoggedIn}
/>
</SheetContent>
</Sheet>
);
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { AlertCircleIcon } from "lucide-react";
import { useState } from "react";
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { cn } from "@/lib/utils";

export function AirdropCSVTable(props: {
data: { address: string; quantity: string; isValid?: boolean }[];
}) {
const pageSize = 10;
const [page, setPage] = useState(1);
const totalPages = Math.ceil(props.data.length / pageSize);
const paginatedData = props.data.slice(
(page - 1) * pageSize,
page * pageSize,
);

return (
<div className="border rounded-lg">
<TableContainer className="border-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Address</TableHead>
<TableHead>Quantity</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{paginatedData.map((row) => (
<TableRow key={row.address}>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Potential duplicate key issue

Using row.address as the key could cause React warnings if there are duplicate addresses in the data. Consider using the index or a combination of address and index.

-{paginatedData.map((row) => (
-  <TableRow key={row.address}>
+{paginatedData.map((row, index) => (
+  <TableRow key={`${row.address}-${index}`}>
🤖 Prompt for AI Agents
In
apps/dashboard/src/app/(app)/(dashboard)/(chain)/[chain_id]/[contractAddress]/tokens/components/airdrop-csv-table.tsx
at line 38, the TableRow key uses row.address which may not be unique and can
cause React warnings. To fix this, update the key to use a combination of
row.address and the row index or just the index to ensure uniqueness and prevent
potential duplicate key issues.

<TableCell>
<div
className={cn(
"flex items-center gap-2",
!row.isValid && "text-red-500",
)}
>
{row.address}
{!row.isValid && (
<AlertCircleIcon className="size-4 text-red-500" />
)}
</div>
</TableCell>
<TableCell>{row.quantity}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
{totalPages > 1 && (
<div className="border-t py-4">
<PaginationButtons
activePage={page}
totalPages={totalPages}
onPageClick={setPage}
/>
</div>
)}
</div>
);
}
Loading
Loading