this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

pagination

+198 -2
+6
src/components/Header.tsx
··· 36 36 Intent Preloading Example 37 37 </Link> 38 38 </div> 39 + 40 + <div className="px-2"> 41 + <Link to="/pagination" activeProps={{ className: "font-bold" }}> 42 + Pagination Example 43 + </Link> 44 + </div> 39 45 </nav> 40 46 </header> 41 47 );
+192 -2
src/routes/pagination.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 1 + import { 2 + queryOptions, 3 + useQueryClient, 4 + useSuspenseQuery, 5 + } from "@tanstack/react-query"; 6 + import { Link, createFileRoute } from "@tanstack/react-router"; 7 + import { buttonVariants } from "~/components/ui/button"; 8 + 9 + import { useMemo } from "react"; 10 + import * as v from "valibot"; 11 + import { 12 + Table, 13 + TableBody, 14 + TableCell, 15 + TableHead, 16 + TableHeader, 17 + TableRow, 18 + } from "~/components/ui/table"; 19 + import { cn } from "~/lib/utils"; 20 + import { $pokeFetchClient } from "../data/client"; 21 + 22 + const POKEMON_LIMIT = 20; 23 + 24 + const matchPokemonIdExp = /\/api\/v2\/pokemon\/(\d+)\/?/; 25 + 26 + const searchParamsSchema = v.object({ 27 + offset: v.optional(v.number(), 0), 28 + }); 2 29 3 30 export const Route = createFileRoute("/pagination")({ 31 + validateSearch: searchParamsSchema, 32 + loaderDeps: ({ search }) => ({ 33 + offset: search.offset, 34 + }), 35 + context: ({ deps }) => { 36 + const newKey = [ 37 + "pokemon-list", 38 + "pagination", 39 + { limit: POKEMON_LIMIT, offset: deps.offset }, 40 + ] as const; 41 + 42 + const pokemonListOptions = queryOptions({ 43 + queryKey: newKey, 44 + queryFn: async ({ queryKey }) => { 45 + const { data, error } = await $pokeFetchClient.GET("/api/v2/pokemon/", { 46 + params: { 47 + query: { 48 + limit: queryKey[2].limit, 49 + offset: queryKey[2].offset, 50 + }, 51 + }, 52 + }); 53 + 54 + if (error) { 55 + throw error; 56 + } 57 + 58 + return data; 59 + }, 60 + }); 61 + 62 + return { 63 + pokemonListOptions, 64 + }; 65 + }, 66 + loader: ({ context }) => { 67 + context.queryClient.prefetchQuery(context.pokemonListOptions); 68 + }, 4 69 component: RouteComponent, 70 + notFoundComponent: NotFoundComponent, 71 + errorComponent: ErrorComponent, 72 + pendingComponent: LoadingComponent, 5 73 }); 6 74 75 + interface PokemonListResult { 76 + name: string; 77 + url: string; 78 + } 79 + 80 + function NotFoundComponent() { 81 + return <div>Not Found</div>; 82 + } 83 + 84 + function ErrorComponent() { 85 + return <div>Error</div>; 86 + } 87 + 88 + function LoadingComponent() { 89 + return <div>Loading...</div>; 90 + } 91 + 7 92 function RouteComponent() { 8 - return <div>Hello "/pagination"!</div>; 93 + const { offset: currentOffset } = Route.useSearch(); 94 + const { pokemonListOptions } = Route.useRouteContext(); 95 + const queryClient = useQueryClient(); 96 + 97 + const { data } = useSuspenseQuery(pokemonListOptions); 98 + 99 + const previousOffset = useMemo(() => { 100 + if (data?.previous == null) { 101 + return null; 102 + } 103 + 104 + return new URL(data.previous).searchParams.get("offset") ?? null; 105 + }, [data?.previous]); 106 + 107 + const nextOffset = useMemo(() => { 108 + if (data?.next == null) { 109 + return null; 110 + } 111 + 112 + return new URL(data.next).searchParams.get("offset") ?? null; 113 + }, [data?.next]); 114 + 115 + if (previousOffset !== null) { 116 + queryClient.prefetchQuery({ 117 + ...pokemonListOptions, 118 + queryKey: [ 119 + "pokemon-list", 120 + "pagination", 121 + { limit: POKEMON_LIMIT, offset: Number(previousOffset) }, 122 + ], 123 + }); 124 + } 125 + 126 + if (nextOffset !== null) { 127 + queryClient.prefetchQuery({ 128 + ...pokemonListOptions, 129 + queryKey: [ 130 + "pokemon-list", 131 + "pagination", 132 + { limit: POKEMON_LIMIT, offset: Number(nextOffset) }, 133 + ], 134 + }); 135 + } 136 + 137 + const results = data.results ?? []; 138 + return ( 139 + <div className="p-4"> 140 + <h1 className="text-2xl font-bold mb-4"> 141 + National Pokédex: Pokémon {currentOffset + 1}- 142 + {currentOffset + POKEMON_LIMIT} 143 + </h1> 144 + <Table> 145 + <TableHeader> 146 + <TableRow> 147 + <TableHead>#</TableHead> 148 + <TableHead>Name</TableHead> 149 + <TableHead>Details</TableHead> 150 + </TableRow> 151 + </TableHeader> 152 + <TableBody> 153 + {results.map((pokemon: PokemonListResult) => ( 154 + <TableRow key={pokemon.name}> 155 + <TableCell>{pokemon.url.match(matchPokemonIdExp)?.[1]}</TableCell> 156 + <TableCell className="capitalize">{pokemon.name}</TableCell> 157 + <TableCell> 158 + <a 159 + href={pokemon.url} 160 + target="_blank" 161 + rel="noopener noreferrer" 162 + className="text-blue-600 underline" 163 + > 164 + View 165 + </a> 166 + </TableCell> 167 + </TableRow> 168 + ))} 169 + </TableBody> 170 + </Table> 171 + <div className="flex justify-center gap-4 mt-4"> 172 + <Link 173 + to="/pagination" 174 + preload="intent" 175 + className={buttonVariants({ 176 + variant: "outline", 177 + className: cn(!previousOffset && "opacity-50 cursor-not-allowed"), 178 + })} 179 + search={{ offset: Number(previousOffset) }} 180 + disabled={!previousOffset} 181 + > 182 + Previous 183 + </Link> 184 + <Link 185 + to="/pagination" 186 + preload="intent" 187 + search={{ offset: Number(nextOffset) }} 188 + className={buttonVariants({ 189 + variant: "outline", 190 + className: cn(!nextOffset && "opacity-50 cursor-not-allowed"), 191 + })} 192 + disabled={!nextOffset} 193 + > 194 + Next 195 + </Link> 196 + </div> 197 + </div> 198 + ); 9 199 }