|
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"; |
25 | 2 | import type { ThirdwebClient } from "thirdweb";
|
26 | 3 | import type { NFTInput } from "thirdweb/utils";
|
| 4 | +import { FilePreview } from "@/components/blocks/file-preview"; |
| 5 | +import { PaginationButtons } from "@/components/blocks/pagination-buttons"; |
27 | 6 | 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"; |
28 | 16 | import { ToolTipLabel } from "@/components/ui/tooltip";
|
29 | 17 |
|
30 |
| -interface BatchTableProps { |
| 18 | +type BatchTableProps = { |
31 | 19 | data: NFTInput[];
|
32 |
| - portalRef: React.RefObject<HTMLDivElement | null>; |
33 | 20 | nextTokenIdToMint?: bigint;
|
34 | 21 | client: ThirdwebClient;
|
35 |
| -} |
| 22 | +}; |
36 | 23 |
|
37 |
| -export const BatchTable: React.FC<BatchTableProps> = ({ |
| 24 | +export function BatchTable({ |
38 | 25 | data,
|
39 |
| - portalRef, |
40 | 26 | nextTokenIdToMint,
|
41 | 27 | 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; |
51 | 31 |
|
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); |
104 | 36 |
|
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 | + }; |
113 | 40 |
|
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; |
137 | 49 |
|
138 | 50 | // Render the UI for your table
|
139 | 51 | 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; |
161 | 72 | 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} |
168 | 76 | >
|
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> |
183 | 166 | );
|
184 | 167 | })}
|
185 |
| - </Tbody> |
| 168 | + </TableBody> |
186 | 169 | </Table>
|
187 | 170 | </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 |
| - /> |
219 | 171 |
|
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 | + /> |
233 | 179 | </div>
|
234 |
| - </Portal> |
235 |
| - </Flex> |
| 180 | + )} |
| 181 | + </div> |
236 | 182 | );
|
237 |
| -}; |
| 183 | +} |
0 commit comments