Skip to content

Commit 5e08575

Browse files
committed
Dashboard: Migrate Batch Upload NFTs to tailwind, UI improvements
1 parent 8260af3 commit 5e08575

File tree

10 files changed

+582
-679
lines changed

10 files changed

+582
-679
lines changed

apps/dashboard/src/@/components/batch-upload/batch-lazy-mint.tsx

Lines changed: 313 additions & 333 deletions
Large diffs are not rendered by default.
Lines changed: 156 additions & 210 deletions
Original file line numberDiff line numberDiff line change
@@ -1,237 +1,183 @@
1-
/** biome-ignore-all lint/nursery/noNestedComponentDefinitions: FIXME */
2-
3-
import { FilePreview } from "@app/team/[team_slug]/[project_slug]/(sidebar)/tokens/create/_common/file-preview";
4-
import {
5-
Flex,
6-
IconButton,
7-
Portal,
8-
Select,
9-
Table,
10-
TableContainer,
11-
Tbody,
12-
Td,
13-
Th,
14-
Thead,
15-
Tr,
16-
} from "@chakra-ui/react";
17-
import {
18-
ChevronFirstIcon,
19-
ChevronLastIcon,
20-
ChevronLeftIcon,
21-
ChevronRightIcon,
22-
} from "lucide-react";
23-
import { useMemo } from "react";
24-
import { type Column, usePagination, useTable } from "react-table";
1+
import { useState } from "react";
252
import type { ThirdwebClient } from "thirdweb";
263
import type { NFTInput } from "thirdweb/utils";
4+
import { FilePreview } from "@/components/blocks/file-preview";
5+
import { PaginationButtons } from "@/components/blocks/pagination-buttons";
276
import { CodeClient } from "@/components/ui/code/code.client";
7+
import {
8+
Table,
9+
TableBody,
10+
TableCell,
11+
TableContainer,
12+
TableHead,
13+
TableHeader,
14+
TableRow,
15+
} from "@/components/ui/table";
2816
import { ToolTipLabel } from "@/components/ui/tooltip";
2917

30-
interface BatchTableProps {
18+
type BatchTableProps = {
3119
data: NFTInput[];
32-
portalRef: React.RefObject<HTMLDivElement | null>;
3320
nextTokenIdToMint?: bigint;
3421
client: ThirdwebClient;
35-
}
22+
};
3623

37-
export const BatchTable: React.FC<BatchTableProps> = ({
24+
export function BatchTable({
3825
data,
39-
portalRef,
4026
nextTokenIdToMint,
4127
client,
42-
}) => {
43-
const columns = useMemo(() => {
44-
let cols: Column<NFTInput>[] = [];
45-
if (nextTokenIdToMint !== undefined) {
46-
cols = cols.concat({
47-
accessor: (_row, index) => String(nextTokenIdToMint + BigInt(index)),
48-
Header: "Token ID",
49-
});
50-
}
28+
}: BatchTableProps) {
29+
const [currentPage, setCurrentPage] = useState(1);
30+
const pageSize = 10;
5131

52-
cols = cols.concat([
53-
{
54-
accessor: (row) => row.image,
55-
Cell: ({ cell: { value } }: { cell: { value?: string } }) => (
56-
<FilePreview
57-
className="size-24 shrink-0 rounded-lg object-contain"
58-
client={client}
59-
srcOrFile={value}
60-
/>
61-
),
62-
Header: "Image",
63-
},
64-
{
65-
accessor: (row) => row.animation_url,
66-
Cell: ({ cell: { value } }: { cell: { value?: string } }) => (
67-
<FilePreview
68-
className="size-24 shrink-0 rounded-lg"
69-
client={client}
70-
srcOrFile={value}
71-
/>
72-
),
73-
Header: "Animation Url",
74-
},
75-
{ accessor: (row) => row.name, Header: "Name" },
76-
{
77-
accessor: (row) => (
78-
<ToolTipLabel label={row.description}>
79-
<p className="line-clamp-6 whitespace-pre-wrap">
80-
{row.description}
81-
</p>
82-
</ToolTipLabel>
83-
),
84-
Header: "Description",
85-
},
86-
{
87-
accessor: (row) => row.attributes || row.properties,
88-
// biome-ignore lint/suspicious/noExplicitAny: FIXME
89-
Cell: ({ cell }: { cell: any }) =>
90-
cell.value ? (
91-
<CodeClient
92-
code={JSON.stringify(cell.value || {}, null, 2)}
93-
lang="json"
94-
scrollableClassName="max-w-[300px]"
95-
/>
96-
) : null,
97-
Header: "Attributes",
98-
},
99-
{ accessor: (row) => row.external_url, Header: "External URL" },
100-
{ accessor: (row) => row.background_color, Header: "Background Color" },
101-
]);
102-
return cols;
103-
}, [nextTokenIdToMint, client]);
32+
const totalPages = Math.ceil(data.length / pageSize);
33+
const startIndex = (currentPage - 1) * pageSize;
34+
const endIndex = startIndex + pageSize;
35+
const currentData = data.slice(startIndex, endIndex);
10436

105-
const {
106-
getTableProps,
107-
getTableBodyProps,
108-
headerGroups,
109-
prepareRow,
110-
// Instead of using 'rows', we'll use page,
111-
page,
112-
// which has only the rows for the active page
37+
const handlePageChange = (page: number) => {
38+
setCurrentPage(page);
39+
};
11340

114-
// The rest of these things are super handy, too ;)
115-
canPreviousPage,
116-
canNextPage,
117-
pageOptions,
118-
pageCount,
119-
gotoPage,
120-
nextPage,
121-
previousPage,
122-
setPageSize,
123-
state: { pageIndex, pageSize },
124-
} = useTable(
125-
{
126-
columns,
127-
data,
128-
initialState: {
129-
pageIndex: 0,
130-
pageSize: 50,
131-
},
132-
},
133-
// will be fixed with @tanstack/react-table v8
134-
// eslint-disable-next-line react-compiler/react-compiler
135-
usePagination,
136-
);
41+
const showTokenId = nextTokenIdToMint !== undefined;
42+
const showExternalUrl = data.some((row) => row.external_url);
43+
const showBackgroundColor = data.some((row) => row.background_color);
44+
const showAnimationUrl = data.some((row) => row.animation_url);
45+
const showDescription = data.some((row) => row.description);
46+
const showAttributes = data.some((row) => row.attributes || row.properties);
47+
48+
const showPagination = totalPages > 1;
13749

13850
// Render the UI for your table
13951
return (
140-
<Flex flexGrow={1} overflow="auto">
141-
<TableContainer className="w-full" maxW="100%">
142-
<Table {...getTableProps()}>
143-
<Thead>
144-
{headerGroups.map((headerGroup, index) => (
145-
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
146-
<Tr {...headerGroup.getHeaderGroupProps()} key={index}>
147-
{headerGroup.headers.map((column, i) => (
148-
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
149-
<Th {...column.getHeaderProps()} border="none" key={i}>
150-
<p className="text-muted-foreground">
151-
{column.render("Header")}
152-
</p>
153-
</Th>
154-
))}
155-
</Tr>
156-
))}
157-
</Thead>
158-
<Tbody {...getTableBodyProps()}>
159-
{page.map((row, rowIndex) => {
160-
prepareRow(row);
52+
<div className="rounded-lg border bg-card">
53+
<TableContainer className="border-0">
54+
<Table>
55+
<TableHeader>
56+
<TableRow>
57+
{showTokenId && <TableHead>Token ID</TableHead>}
58+
<TableHead>Image</TableHead>
59+
{showAnimationUrl && <TableHead>Animation Url</TableHead>}
60+
<TableHead>Name</TableHead>
61+
{showDescription && (
62+
<TableHead className="min-w-[300px]">Description</TableHead>
63+
)}
64+
{showAttributes && <TableHead>Attributes</TableHead>}
65+
{showExternalUrl && <TableHead>External URL</TableHead>}
66+
{showBackgroundColor && <TableHead>Background Color</TableHead>}
67+
</TableRow>
68+
</TableHeader>
69+
<TableBody>
70+
{currentData.map((row, rowIndex) => {
71+
const actualIndex = startIndex + rowIndex;
16172
return (
162-
<Tr
163-
{...row.getRowProps()}
164-
_last={{ borderBottomWidth: 0 }}
165-
borderBottomWidth={1}
166-
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
167-
key={rowIndex}
73+
<TableRow
74+
className="border-b last:border-b-0"
75+
key={actualIndex}
16876
>
169-
{row.cells.map((cell, cellIndex) => {
170-
return (
171-
<Td
172-
{...cell.getCellProps()}
173-
borderBottomWidth="inherit"
174-
borderColor="borderColor"
175-
// biome-ignore lint/suspicious/noArrayIndexKey: FIXME
176-
key={cellIndex}
177-
>
178-
{cell.render("Cell")}
179-
</Td>
180-
);
181-
})}
182-
</Tr>
77+
{/* Token ID */}
78+
{showTokenId && (
79+
<TableCell>
80+
{String(nextTokenIdToMint + BigInt(actualIndex))}
81+
</TableCell>
82+
)}
83+
84+
{/* Image */}
85+
<TableCell className="min-w-36">
86+
<FilePreview
87+
className="size-36 shrink-0 rounded-lg object-contain"
88+
client={client}
89+
srcOrFile={
90+
typeof row.image === "string" ||
91+
row.image instanceof File
92+
? row.image
93+
: undefined
94+
}
95+
/>
96+
</TableCell>
97+
98+
{/* Animation Url */}
99+
{showAnimationUrl && (
100+
<TableCell>
101+
<FilePreview
102+
className="size-24 shrink-0 rounded-lg"
103+
client={client}
104+
srcOrFile={
105+
typeof row.animation_url === "string" ||
106+
row.animation_url instanceof File
107+
? row.animation_url
108+
: undefined
109+
}
110+
/>
111+
</TableCell>
112+
)}
113+
114+
{/* Name */}
115+
<TableCell>{row.name}</TableCell>
116+
117+
{/* Description */}
118+
{showDescription && (
119+
<TableCell className="max-w-xs">
120+
<p className="whitespace-pre-wrap">{row.description}</p>
121+
</TableCell>
122+
)}
123+
124+
{/* Attributes */}
125+
{showAttributes && (
126+
<TableCell>
127+
{row.attributes || row.properties ? (
128+
<CodeClient
129+
code={JSON.stringify(
130+
row.attributes || row.properties || {},
131+
null,
132+
2,
133+
)}
134+
lang="json"
135+
scrollableClassName="max-w-[300px] max-h-[400px]"
136+
className="bg-background"
137+
/>
138+
) : null}
139+
</TableCell>
140+
)}
141+
142+
{/* External URL */}
143+
{showExternalUrl && (
144+
<TableCell>
145+
{typeof row.external_url === "string" ? (
146+
<ToolTipLabel label={row.external_url}>
147+
<span>
148+
{row.external_url.slice(0, 20) +
149+
(row.external_url.length > 20 ? "..." : "")}
150+
</span>
151+
</ToolTipLabel>
152+
) : row.external_url instanceof File ? (
153+
<FilePreview
154+
client={client}
155+
srcOrFile={row.external_url}
156+
/>
157+
) : null}
158+
</TableCell>
159+
)}
160+
161+
{/* Background Color */}
162+
{showBackgroundColor && (
163+
<TableCell>{row.background_color}</TableCell>
164+
)}
165+
</TableRow>
183166
);
184167
})}
185-
</Tbody>
168+
</TableBody>
186169
</Table>
187170
</TableContainer>
188-
<Portal containerRef={portalRef}>
189-
<div className="flex w-full items-center justify-center">
190-
<div className="flex flex-row items-center gap-2">
191-
<IconButton
192-
aria-label="first page"
193-
icon={<ChevronFirstIcon className="size-4" />}
194-
isDisabled={!canPreviousPage}
195-
onClick={() => gotoPage(0)}
196-
/>
197-
<IconButton
198-
aria-label="previous page"
199-
icon={<ChevronLeftIcon className="size-4" />}
200-
isDisabled={!canPreviousPage}
201-
onClick={() => previousPage()}
202-
/>
203-
<p className="whitespace-nowrap">
204-
Page <strong>{pageIndex + 1}</strong> of{" "}
205-
<strong>{pageOptions.length}</strong>
206-
</p>
207-
<IconButton
208-
aria-label="next page"
209-
icon={<ChevronRightIcon className="size-4" />}
210-
isDisabled={!canNextPage}
211-
onClick={() => nextPage()}
212-
/>
213-
<IconButton
214-
aria-label="last page"
215-
icon={<ChevronLastIcon className="size-4" />}
216-
isDisabled={!canNextPage}
217-
onClick={() => gotoPage(pageCount - 1)}
218-
/>
219171

220-
<Select
221-
onChange={(e) => {
222-
setPageSize(Number.parseInt(e.target.value as string, 10));
223-
}}
224-
value={pageSize}
225-
>
226-
<option value="25">25</option>
227-
<option value="50">50</option>
228-
<option value="100">100</option>
229-
<option value="250">250</option>
230-
<option value="500">500</option>
231-
</Select>
232-
</div>
172+
{showPagination && (
173+
<div className="border-t py-5">
174+
<PaginationButtons
175+
activePage={currentPage}
176+
totalPages={totalPages}
177+
onPageClick={handlePageChange}
178+
/>
233179
</div>
234-
</Portal>
235-
</Flex>
180+
)}
181+
</div>
236182
);
237-
};
183+
}

0 commit comments

Comments
 (0)