this repo has no description
0
fork

Configure Feed

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

suspense

+208 -45
+13 -3
src/components/Header.tsx
··· 4 4 return ( 5 5 <header className="p-2 flex gap-2 bg-white text-black justify-between"> 6 6 <nav className="flex flex-row"> 7 - <div className="px-2 font-bold"> 8 - <Link to="/">Home</Link> 7 + <div className="px-2"> 8 + <Link to="/" activeProps={{ className: "font-bold" }}> 9 + Home 10 + </Link> 9 11 </div> 10 12 11 13 <div className="px-2"> 12 - <Link to="/basic">Basic Example</Link> 14 + <Link to="/basic" activeProps={{ className: "font-bold" }}> 15 + Basic Example 16 + </Link> 17 + </div> 18 + 19 + <div className="px-2"> 20 + <Link to="/suspense" activeProps={{ className: "font-bold" }}> 21 + Suspense Example 22 + </Link> 13 23 </div> 14 24 </nav> 15 25 </header>
+1 -3
src/data/client.ts
··· 1 1 import createFetchClient from "openapi-fetch"; 2 - import createClient from "openapi-react-query"; 3 2 import type { paths } from "./schema"; // generated by openapi-typescript 4 3 5 - const fetchClient = createFetchClient<paths>({ 4 + export const $pokeFetchClient = createFetchClient<paths>({ 6 5 baseUrl: "https://pokeapi.co/", 7 6 }); 8 - export const $pokeApiClient = createClient(fetchClient);
+20 -20
src/routeTree.gen.ts
··· 11 11 // Import Routes 12 12 13 13 import { Route as rootRoute } from './routes/__root' 14 - import { Route as SuspenceImport } from './routes/suspence' 14 + import { Route as SuspenseImport } from './routes/suspense' 15 15 import { Route as SearchDebouncingImport } from './routes/search-debouncing' 16 16 import { Route as PreloadingImport } from './routes/preloading' 17 17 import { Route as PaginationImport } from './routes/pagination' ··· 21 21 22 22 // Create/Update Routes 23 23 24 - const SuspenceRoute = SuspenceImport.update({ 25 - id: '/suspence', 26 - path: '/suspence', 24 + const SuspenseRoute = SuspenseImport.update({ 25 + id: '/suspense', 26 + path: '/suspense', 27 27 getParentRoute: () => rootRoute, 28 28 } as any) 29 29 ··· 109 109 preLoaderRoute: typeof SearchDebouncingImport 110 110 parentRoute: typeof rootRoute 111 111 } 112 - '/suspence': { 113 - id: '/suspence' 114 - path: '/suspence' 115 - fullPath: '/suspence' 116 - preLoaderRoute: typeof SuspenceImport 112 + '/suspense': { 113 + id: '/suspense' 114 + path: '/suspense' 115 + fullPath: '/suspense' 116 + preLoaderRoute: typeof SuspenseImport 117 117 parentRoute: typeof rootRoute 118 118 } 119 119 } ··· 128 128 '/pagination': typeof PaginationRoute 129 129 '/preloading': typeof PreloadingRoute 130 130 '/search-debouncing': typeof SearchDebouncingRoute 131 - '/suspence': typeof SuspenceRoute 131 + '/suspense': typeof SuspenseRoute 132 132 } 133 133 134 134 export interface FileRoutesByTo { ··· 138 138 '/pagination': typeof PaginationRoute 139 139 '/preloading': typeof PreloadingRoute 140 140 '/search-debouncing': typeof SearchDebouncingRoute 141 - '/suspence': typeof SuspenceRoute 141 + '/suspense': typeof SuspenseRoute 142 142 } 143 143 144 144 export interface FileRoutesById { ··· 149 149 '/pagination': typeof PaginationRoute 150 150 '/preloading': typeof PreloadingRoute 151 151 '/search-debouncing': typeof SearchDebouncingRoute 152 - '/suspence': typeof SuspenceRoute 152 + '/suspense': typeof SuspenseRoute 153 153 } 154 154 155 155 export interface FileRouteTypes { ··· 161 161 | '/pagination' 162 162 | '/preloading' 163 163 | '/search-debouncing' 164 - | '/suspence' 164 + | '/suspense' 165 165 fileRoutesByTo: FileRoutesByTo 166 166 to: 167 167 | '/' ··· 170 170 | '/pagination' 171 171 | '/preloading' 172 172 | '/search-debouncing' 173 - | '/suspence' 173 + | '/suspense' 174 174 id: 175 175 | '__root__' 176 176 | '/' ··· 179 179 | '/pagination' 180 180 | '/preloading' 181 181 | '/search-debouncing' 182 - | '/suspence' 182 + | '/suspense' 183 183 fileRoutesById: FileRoutesById 184 184 } 185 185 ··· 190 190 PaginationRoute: typeof PaginationRoute 191 191 PreloadingRoute: typeof PreloadingRoute 192 192 SearchDebouncingRoute: typeof SearchDebouncingRoute 193 - SuspenceRoute: typeof SuspenceRoute 193 + SuspenseRoute: typeof SuspenseRoute 194 194 } 195 195 196 196 const rootRouteChildren: RootRouteChildren = { ··· 200 200 PaginationRoute: PaginationRoute, 201 201 PreloadingRoute: PreloadingRoute, 202 202 SearchDebouncingRoute: SearchDebouncingRoute, 203 - SuspenceRoute: SuspenceRoute, 203 + SuspenseRoute: SuspenseRoute, 204 204 } 205 205 206 206 export const routeTree = rootRoute ··· 219 219 "/pagination", 220 220 "/preloading", 221 221 "/search-debouncing", 222 - "/suspence" 222 + "/suspense" 223 223 ] 224 224 }, 225 225 "/": { ··· 240 240 "/search-debouncing": { 241 241 "filePath": "search-debouncing.tsx" 242 242 }, 243 - "/suspence": { 244 - "filePath": "suspence.tsx" 243 + "/suspense": { 244 + "filePath": "suspense.tsx" 245 245 } 246 246 } 247 247 }
+25 -10
src/routes/basic.tsx
··· 13 13 TableRow, 14 14 } from "~/components/ui/table"; 15 15 import { cn } from "~/lib/utils"; 16 - import { $pokeApiClient } from "../data/client"; 16 + import { $pokeFetchClient } from "../data/client"; 17 17 18 18 const POKEMON_LIMIT = 20; 19 19 ··· 36 36 function RouteComponent() { 37 37 const { offset: currentOffset } = Route.useSearch(); 38 38 39 - const { data, error } = useQuery( 40 - $pokeApiClient.queryOptions("get", "/api/v2/pokemon/", { 41 - params: { 42 - query: { 43 - limit: POKEMON_LIMIT, 44 - offset: currentOffset, 39 + const newKey = [ 40 + "pokemon-list", 41 + "basic", 42 + { limit: POKEMON_LIMIT, offset: currentOffset }, 43 + ] as const; 44 + 45 + const { data, error } = useQuery({ 46 + queryKey: newKey, 47 + queryFn: async () => { 48 + const { data, error } = await $pokeFetchClient.GET("/api/v2/pokemon/", { 49 + params: { 50 + query: { 51 + limit: POKEMON_LIMIT, 52 + offset: currentOffset, 53 + }, 45 54 }, 46 - }, 47 - }), 48 - ); 55 + }); 56 + 57 + if (error) { 58 + throw error; 59 + } 60 + 61 + return data; 62 + }, 63 + }); 49 64 50 65 const previousOffset = useMemo(() => { 51 66 if (data?.previous == null) {
-9
src/routes/suspence.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 2 - 3 - export const Route = createFileRoute("/suspence")({ 4 - component: RouteComponent, 5 - }); 6 - 7 - function RouteComponent() { 8 - return <div>Hello "/suspence"!</div>; 9 - }
+149
src/routes/suspense.tsx
··· 1 + import { useQuery } from "@tanstack/react-query"; 2 + import { Link, createFileRoute } from "@tanstack/react-router"; 3 + import { buttonVariants } from "~/components/ui/button"; 4 + 5 + import { useMemo } from "react"; 6 + import * as v from "valibot"; 7 + import { 8 + Table, 9 + TableBody, 10 + TableCell, 11 + TableHead, 12 + TableHeader, 13 + TableRow, 14 + } from "~/components/ui/table"; 15 + import { cn } from "~/lib/utils"; 16 + import { $pokeFetchClient } from "../data/client"; 17 + 18 + const POKEMON_LIMIT = 20; 19 + 20 + const matchPokemonIdExp = /\/api\/v2\/pokemon\/(\d+)\/?/; 21 + 22 + const searchParamsSchema = v.object({ 23 + offset: v.optional(v.number(), 0), 24 + }); 25 + 26 + export const Route = createFileRoute("/suspense")({ 27 + validateSearch: searchParamsSchema, 28 + component: RouteComponent, 29 + }); 30 + 31 + interface PokemonListResult { 32 + name: string; 33 + url: string; 34 + } 35 + 36 + function RouteComponent() { 37 + const { offset: currentOffset } = Route.useSearch(); 38 + 39 + const newKey = [ 40 + "pokemon-list", 41 + "suspense", 42 + { limit: POKEMON_LIMIT, offset: currentOffset }, 43 + ] as const; 44 + 45 + const { data, error } = useQuery({ 46 + queryKey: newKey, 47 + queryFn: async () => { 48 + const { data, error } = await $pokeFetchClient.GET("/api/v2/pokemon/", { 49 + params: { 50 + query: { 51 + limit: POKEMON_LIMIT, 52 + offset: currentOffset, 53 + }, 54 + }, 55 + }); 56 + 57 + if (error) { 58 + throw error; 59 + } 60 + 61 + return data; 62 + }, 63 + }); 64 + 65 + const previousOffset = useMemo(() => { 66 + if (data?.previous == null) { 67 + return null; 68 + } 69 + 70 + return new URL(data.previous).searchParams.get("offset") ?? null; 71 + }, [data?.previous]); 72 + 73 + const nextOffset = useMemo(() => { 74 + if (data?.next == null) { 75 + return null; 76 + } 77 + 78 + return new URL(data.next).searchParams.get("offset") ?? null; 79 + }, [data?.next]); 80 + 81 + if (data) { 82 + const results = data.results ?? []; 83 + return ( 84 + <div className="p-4"> 85 + <h1 className="text-2xl font-bold mb-4"> 86 + National Pokédex: Pokémon {currentOffset + 1}- 87 + {currentOffset + POKEMON_LIMIT} 88 + </h1> 89 + <Table> 90 + <TableHeader> 91 + <TableRow> 92 + <TableHead>#</TableHead> 93 + <TableHead>Name</TableHead> 94 + <TableHead>Details</TableHead> 95 + </TableRow> 96 + </TableHeader> 97 + <TableBody> 98 + {results.map((pokemon: PokemonListResult) => ( 99 + <TableRow key={pokemon.name}> 100 + <TableCell> 101 + {pokemon.url.match(matchPokemonIdExp)?.[1]} 102 + </TableCell> 103 + <TableCell className="capitalize">{pokemon.name}</TableCell> 104 + <TableCell> 105 + <a 106 + href={pokemon.url} 107 + target="_blank" 108 + rel="noopener noreferrer" 109 + className="text-blue-600 underline" 110 + > 111 + View 112 + </a> 113 + </TableCell> 114 + </TableRow> 115 + ))} 116 + </TableBody> 117 + </Table> 118 + <div className="flex justify-center gap-4 mt-4"> 119 + <Link 120 + to="/basic" 121 + className={buttonVariants({ 122 + variant: "outline", 123 + className: cn(!previousOffset && "opacity-50 cursor-not-allowed"), 124 + })} 125 + search={{ offset: Number(previousOffset) }} 126 + disabled={!previousOffset} 127 + > 128 + Previous 129 + </Link> 130 + <Link 131 + to="/basic" 132 + search={{ offset: Number(nextOffset) }} 133 + className={buttonVariants({ 134 + variant: "outline", 135 + className: cn(!nextOffset && "opacity-50 cursor-not-allowed"), 136 + })} 137 + disabled={!nextOffset} 138 + > 139 + Next 140 + </Link> 141 + </div> 142 + </div> 143 + ); 144 + } 145 + 146 + if (error) return <div>Error loading Pokémon.</div>; 147 + 148 + return <div>No data</div>; 149 + }