|
| 1 | +'use client' |
| 2 | + |
| 3 | +import { CodeBlock } from '@/src/components/code-block.client' |
| 4 | +import { Querystring } from '@/src/components/querystring' |
| 5 | +import { Label } from '@/src/components/ui/label' |
| 6 | +import { |
| 7 | + Pagination, |
| 8 | + PaginationButton, |
| 9 | + PaginationContent, |
| 10 | + PaginationItem, |
| 11 | + PaginationNext, |
| 12 | + PaginationPrevious |
| 13 | +} from '@/src/components/ui/pagination' |
| 14 | +import { |
| 15 | + Select, |
| 16 | + SelectContent, |
| 17 | + SelectItem, |
| 18 | + SelectTrigger, |
| 19 | + SelectValue |
| 20 | +} from '@/src/components/ui/select' |
| 21 | +import { Separator } from '@/src/components/ui/separator' |
| 22 | +import { |
| 23 | + createParser, |
| 24 | + parseAsInteger, |
| 25 | + useQueryState, |
| 26 | + parseAsArrayOf |
| 27 | +} from 'nuqs' |
| 28 | +import { useDeferredValue, useState } from 'react' |
| 29 | + |
| 30 | +const NUM_PAGES = 5 |
| 31 | + |
| 32 | +// The parser is zero-indexed internally, |
| 33 | +// but one-indexed when rendered in the URL, |
| 34 | +// to align with your UI and what users might expect. |
| 35 | +const paginationParser = createParser({ |
| 36 | + parse(query) { |
| 37 | + const pagination = parseAsArrayOf(parseAsInteger).parse(query); |
| 38 | + |
| 39 | + if (pagination === null) return null |
| 40 | + |
| 41 | + const [pageIndex, pageSize] = pagination |
| 42 | + |
| 43 | + return { pageIndex: pageIndex - 1, pageSize }; |
| 44 | + }, |
| 45 | + serialize({ pageIndex, pageSize }) { |
| 46 | + return parseAsArrayOf(parseAsInteger).serialize([pageIndex + 1, pageSize]); |
| 47 | + }, |
| 48 | + eq({ pageIndex, pageSize }) { |
| 49 | + return pageIndex === 0 && pageSize === 10; |
| 50 | + }, |
| 51 | +}); |
| 52 | + |
| 53 | +type Pagination = {pageIndex: number; pageSize: number} |
| 54 | + |
| 55 | +const PaginationComponent = ({pagination, onPaginationChange}: |
| 56 | + {pagination:Pagination ;onPaginationChange: React.Dispatch<React.SetStateAction<Pagination>>}) => { |
| 57 | + const { pageIndex: page, pageSize } = pagination |
| 58 | + const setPage = (pageIndex: number) => { |
| 59 | + onPaginationChange((prev) => ({ |
| 60 | + ...prev, pageIndex |
| 61 | + })) |
| 62 | + } |
| 63 | + const setPageSize = (pageSize: number) => { |
| 64 | + onPaginationChange((prev) => ({ |
| 65 | + ...prev, pageSize |
| 66 | + })) |
| 67 | + } |
| 68 | + |
| 69 | + return <div className="flex flex-wrap items-center justify-start gap-2 rounded-xl border border-dashed p-1"> |
| 70 | + <Pagination className="not-prose mx-0 w-auto items-center gap-2"> |
| 71 | + <PaginationContent> |
| 72 | + <PaginationItem> |
| 73 | + <PaginationPrevious |
| 74 | + disabled={page <= 0} |
| 75 | + onClick={() => setPage(Math.max(0, page - 1))} |
| 76 | + /> |
| 77 | + </PaginationItem> |
| 78 | + {Array.from({ length: NUM_PAGES }, (_, index) => ( |
| 79 | + <PaginationItem key={index}> |
| 80 | + <PaginationButton |
| 81 | + isActive={page === index} |
| 82 | + onClick={() => setPage(index)} |
| 83 | + > |
| 84 | + {index + 1} |
| 85 | + </PaginationButton> |
| 86 | + </PaginationItem> |
| 87 | + ))} |
| 88 | + <PaginationItem> |
| 89 | + <PaginationNext |
| 90 | + disabled={page >= NUM_PAGES - 1} |
| 91 | + onClick={() => setPage(Math.min(NUM_PAGES - 1, page + 1))} |
| 92 | + /> |
| 93 | + </PaginationItem> |
| 94 | + </PaginationContent> |
| 95 | + </Pagination> |
| 96 | + <Label className="ml-auto flex items-center gap-2"> |
| 97 | + Items per page |
| 98 | + <Select |
| 99 | + value={pageSize.toFixed()} |
| 100 | + onValueChange={value => setPageSize(parseInt(value))} |
| 101 | + > |
| 102 | + <SelectTrigger className="w-24"> |
| 103 | + <SelectValue placeholder="10" /> |
| 104 | + </SelectTrigger> |
| 105 | + <SelectContent> |
| 106 | + <SelectItem value="10">10</SelectItem> |
| 107 | + <SelectItem value="25">25</SelectItem> |
| 108 | + <SelectItem value="50">50</SelectItem> |
| 109 | + <SelectItem value="100">100</SelectItem> |
| 110 | + </SelectContent> |
| 111 | + </Select> |
| 112 | + </Label> |
| 113 | + </div> |
| 114 | +} |
| 115 | + |
| 116 | +export function TanStackTableMultiplePagination() { |
| 117 | + const [firstPaginationKey, setFirstPaginationKey] = useState('p1') |
| 118 | + const [secondPaginationKey, setSecondPaginationKey] = useState('p2') |
| 119 | + const [thirdPaginationKey, setThirdPaginationKey] = useState('p3') |
| 120 | + |
| 121 | + const paginationParserWithDefaults = paginationParser.withDefault({ pageIndex: 0, pageSize: 10 }) |
| 122 | + |
| 123 | + const [firstPagination, setFirstPagination] = useQueryState( |
| 124 | + firstPaginationKey, |
| 125 | + paginationParserWithDefaults |
| 126 | + ) |
| 127 | + const [secondPagination, setSecondPagination] = useQueryState( |
| 128 | + secondPaginationKey, |
| 129 | + paginationParserWithDefaults |
| 130 | + ) |
| 131 | + const [thirdPagination, setThirdPagination] = useQueryState( |
| 132 | + thirdPaginationKey, |
| 133 | + paginationParserWithDefaults |
| 134 | + ) |
| 135 | + |
| 136 | + |
| 137 | + const parserCode = useDeferredValue(`import { |
| 138 | + createParser, |
| 139 | + parseAsInteger, |
| 140 | + useQueryState, |
| 141 | + parseAsArrayOf |
| 142 | +} from 'nuqs' |
| 143 | +
|
| 144 | + // The parser is zero-indexed internally, |
| 145 | + // but one-indexed when rendered in the URL, |
| 146 | + // to align with your UI and what users might expect. |
| 147 | + const paginationParser = createParser({ |
| 148 | + parse(query) { |
| 149 | + const pagination = parseAsArrayOf(parseAsInteger).parse(query); |
| 150 | + |
| 151 | + if (pagination === null) return null |
| 152 | + |
| 153 | + const [pageIndex, pageSize] = pagination |
| 154 | + |
| 155 | + return { pageIndex: pageIndex - 1, pageSize }; |
| 156 | + }, |
| 157 | + serialize({ pageIndex, pageSize }) { |
| 158 | + return parseAsArrayOf(parseAsInteger).serialize([pageIndex + 1, pageSize]); |
| 159 | + }, |
| 160 | + eq({ pageIndex, pageSize }) { |
| 161 | + return pageIndex === 0 && pageSize === 10; |
| 162 | + }, |
| 163 | + }); |
| 164 | +
|
| 165 | + const paginationParserWithDefaults = paginationParser.withDefault({pageIndex: 0, pageSize: 10}) |
| 166 | + |
| 167 | + const [firstPagination, setFirstPagination] = useQueryState( |
| 168 | + '${firstPaginationKey}', |
| 169 | + paginationParserWithDefaults |
| 170 | + ) |
| 171 | + const [secondPagination, setSecondPagination] = useQueryState( |
| 172 | + '${secondPaginationKey}', |
| 173 | + paginationParserWithDefaults |
| 174 | + ) |
| 175 | + const [thirdPagination, setThirdPagination] = useQueryState( |
| 176 | + '${thirdPaginationKey}', |
| 177 | + paginationParserWithDefaults |
| 178 | + ) |
| 179 | +}`) |
| 180 | + |
| 181 | + const internalState = useDeferredValue(`{ |
| 182 | + // zero-indexed |
| 183 | + ${firstPaginationKey}: ${JSON.stringify(firstPagination, null, 2)}, |
| 184 | + ${secondPaginationKey}: ${JSON.stringify(secondPagination, null, 2)}, |
| 185 | + ${thirdPaginationKey}: ${JSON.stringify(thirdPagination, null, 2)} |
| 186 | +}`) |
| 187 | + |
| 188 | + return ( |
| 189 | + <section> |
| 190 | + <div className='flex flex-col gap-2 p-2 border border-dashed'> |
| 191 | + <PaginationComponent pagination={firstPagination} onPaginationChange={setFirstPagination}/> |
| 192 | + <PaginationComponent pagination={secondPagination} onPaginationChange={setSecondPagination}/> |
| 193 | + <PaginationComponent pagination={thirdPagination} onPaginationChange={setThirdPagination}/> |
| 194 | + </div> |
| 195 | + <p className="mb-0"> |
| 196 | + Configure and copy-paste this parser into your application: |
| 197 | + </p> |
| 198 | + <div className="flex flex-col gap-6 xl:flex-row"> |
| 199 | + <CodeBlock |
| 200 | + title="search-params.pagination.ts" |
| 201 | + lang="ts" |
| 202 | + icon={ |
| 203 | + <svg |
| 204 | + fill="none" |
| 205 | + viewBox="0 0 128 128" |
| 206 | + xmlns="http://www.w3.org/2000/svg" |
| 207 | + role="presentation" |
| 208 | + > |
| 209 | + <rect fill="currentColor" height="128" rx="6" width="128" /> |
| 210 | + <path |
| 211 | + clipRule="evenodd" |
| 212 | + d="m74.2622 99.468v14.026c2.2724 1.168 4.9598 2.045 8.0625 2.629 3.1027.585 6.3728.877 9.8105.877 3.3503 0 6.533-.321 9.5478-.964 3.016-.643 5.659-1.702 7.932-3.178 2.272-1.476 4.071-3.404 5.397-5.786 1.325-2.381 1.988-5.325 1.988-8.8313 0-2.5421-.379-4.7701-1.136-6.6841-.758-1.9139-1.85-3.6159-3.278-5.1062-1.427-1.4902-3.139-2.827-5.134-4.0104-1.996-1.1834-4.246-2.3011-6.752-3.353-1.8352-.7597-3.4812-1.4975-4.9378-2.2134-1.4567-.7159-2.6948-1.4464-3.7144-2.1915-1.0197-.7452-1.8063-1.5341-2.3598-2.3669-.5535-.8327-.8303-1.7751-.8303-2.827 0-.9643.2476-1.8336.7429-2.6079s1.1945-1.4391 2.0976-1.9943c.9031-.5551 2.0101-.9861 3.3211-1.2929 1.311-.3069 2.7676-.4603 4.3699-.4603 1.1658 0 2.3958.0877 3.6928.263 1.296.1753 2.6.4456 3.911.8109 1.311.3652 2.585.8254 3.824 1.3806 1.238.5552 2.381 1.198 3.43 1.9285v-13.1051c-2.127-.8182-4.45-1.4245-6.97-1.819s-5.411-.5917-8.6744-.5917c-3.3211 0-6.4674.3579-9.439 1.0738-2.9715.7159-5.5862 1.8336-7.844 3.353-2.2578 1.5195-4.0422 3.4553-5.3531 5.8075-1.311 2.3522-1.9665 5.1646-1.9665 8.4373 0 4.1785 1.2017 7.7433 3.6052 10.6945 2.4035 2.9513 6.0523 5.4496 10.9466 7.495 1.9228.7889 3.7145 1.5633 5.375 2.323 1.6606.7597 3.0954 1.5486 4.3044 2.3668s2.1628 1.7094 2.8618 2.6736c.7.9643 1.049 2.06 1.049 3.2873 0 .9062-.218 1.7462-.655 2.5202s-1.1 1.446-1.9885 2.016c-.8886.57-1.9956 1.016-3.3212 1.337-1.3255.321-2.8768.482-4.6539.482-3.0299 0-6.0305-.533-9.0021-1.6-2.9715-1.066-5.7245-2.666-8.2591-4.799zm-23.5596-34.9136h18.2974v-11.5544h-51v11.5544h18.2079v51.4456h14.4947z" |
| 213 | + className="fill-background" |
| 214 | + fillRule="evenodd" |
| 215 | + /> |
| 216 | + </svg> |
| 217 | + } |
| 218 | + className="flex-grow" |
| 219 | + code={parserCode} |
| 220 | + /> |
| 221 | + <aside className="w-full space-y-4 xl:w-64"> |
| 222 | + <Querystring |
| 223 | + value={`?${firstPaginationKey}=[${firstPagination.pageIndex + 1},${firstPagination.pageSize}]&${secondPaginationKey}=[${secondPagination.pageIndex + 1},${secondPagination.pageSize}]&${thirdPaginationKey}=[${thirdPagination.pageIndex + 1},${thirdPagination.pageSize}]`} |
| 224 | + /> |
| 225 | + <CodeBlock |
| 226 | + allowCopy={false} |
| 227 | + title="Internal state" |
| 228 | + code={internalState} |
| 229 | + /> |
| 230 | + <Separator className="my-8" /> |
| 231 | + <div className="space-y-2"> |
| 232 | + <Label htmlFor="firstKey">First pagination URL key</Label> |
| 233 | + <input |
| 234 | + id="firstKey" |
| 235 | + className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" |
| 236 | + value={firstPaginationKey} |
| 237 | + onChange={e => { |
| 238 | + setFirstPagination({ pageIndex: 0, pageSize: 10 }) |
| 239 | + setFirstPaginationKey(e.target.value) |
| 240 | + }} |
| 241 | + placeholder="e.g., page" |
| 242 | + /> |
| 243 | + </div> |
| 244 | + <div className="space-y-2"> |
| 245 | + <Label htmlFor="secondKey">Second pagination URL key</Label> |
| 246 | + <input |
| 247 | + id="secondKey" |
| 248 | + className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" |
| 249 | + value={secondPaginationKey} |
| 250 | + onChange={e => { |
| 251 | + setSecondPagination({ pageIndex: 0, pageSize: 10 }) |
| 252 | + setSecondPaginationKey(e.target.value) |
| 253 | + }} |
| 254 | + placeholder="e.g., page" |
| 255 | + /> |
| 256 | + </div> |
| 257 | + <div className="space-y-2"> |
| 258 | + <Label htmlFor="thirdKey">Second pagination URL key</Label> |
| 259 | + <input |
| 260 | + id="thirdKey" |
| 261 | + className="flex h-10 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" |
| 262 | + value={thirdPaginationKey} |
| 263 | + onChange={e => { |
| 264 | + setThirdPagination({ pageIndex: 0, pageSize: 10 }) |
| 265 | + setThirdPaginationKey(e.target.value) |
| 266 | + }} |
| 267 | + placeholder="e.g., page" |
| 268 | + /> |
| 269 | + </div> |
| 270 | + </aside> |
| 271 | + </div> |
| 272 | + </section> |
| 273 | + ) |
| 274 | +} |
0 commit comments