|
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