this repo has no description
0
fork

Configure Feed

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

routes

+400 -99
+132
src/components/console/pokemon-table-skeleton.tsx
··· 1 + import { Suspense, type ReactNode } from "react"; 2 + import { 3 + Table, 4 + TableBody, 5 + TableCell, 6 + TableHead, 7 + TableHeader, 8 + TableRow, 9 + } from "~/components/ui/table"; 10 + import { cn } from "~/lib/utils"; 11 + 12 + interface PokemonTableSkeletonProps { 13 + /** 14 + * Number of skeleton rows to render (defaults to 10 for POKEMON_LIMIT) 15 + */ 16 + rowCount?: number; 17 + className?: string; 18 + } 19 + 20 + /** 21 + * Skeleton loading state for the Pokemon table. 22 + * 23 + * Renders skeleton rows that exactly match the height of real table rows 24 + * to prevent layout shift when data loads. Uses the same padding and 25 + * structure as the actual PokemonTable component. 26 + * 27 + * @example 28 + * <PokemonTableSkeleton rowCount={10} /> 29 + */ 30 + export function PokemonTableSkeleton({ rowCount = 10, className }: PokemonTableSkeletonProps) { 31 + return ( 32 + <Table className={cn("w-full border-collapse", "text-sm", "font-mono", className)}> 33 + <TableHeader> 34 + <TableRow className="border-b border-(--border-strong)"> 35 + <TableHead 36 + className={cn( 37 + "text-left py-3 px-3", 38 + "font-semibold text-(--text-secondary)", 39 + "uppercase text-xs tracking-wider", 40 + )} 41 + > 42 + # 43 + </TableHead> 44 + <TableHead 45 + className={cn( 46 + "text-left py-3 px-3", 47 + "font-semibold text-(--text-secondary)", 48 + "uppercase text-xs tracking-wider", 49 + )} 50 + > 51 + Name 52 + </TableHead> 53 + <TableHead 54 + className={cn( 55 + "text-left py-3 px-3", 56 + "font-semibold text-(--text-secondary)", 57 + "uppercase text-xs tracking-wider", 58 + )} 59 + > 60 + Details 61 + </TableHead> 62 + </TableRow> 63 + </TableHeader> 64 + <TableBody> 65 + {Array.from({ length: rowCount }).map((_, index) => ( 66 + <SkeletonRow key={index} /> 67 + ))} 68 + </TableBody> 69 + </Table> 70 + ); 71 + } 72 + 73 + /** 74 + * Skeleton row that exactly matches the height of a real table row. 75 + * 76 + * Real rows have py-3 (12px) padding + content. This skeleton uses 77 + * identical padding to ensure zero layout shift when data loads. 78 + */ 79 + function SkeletonRow() { 80 + return ( 81 + <TableRow className="border-b border-(--border-default)"> 82 + <TableCell className="py-3 px-3"> 83 + <div className="h-4 w-8 bg-(--bg-secondary) animate-pulse" /> 84 + </TableCell> 85 + <TableCell className="py-3 px-3"> 86 + <div className="h-4 w-32 bg-(--bg-secondary) animate-pulse" /> 87 + </TableCell> 88 + <TableCell className="py-3 px-3"> 89 + <div className="flex gap-2"> 90 + <div className="h-5 w-16 bg-(--bg-secondary) animate-pulse" /> 91 + <div className="h-5 w-16 bg-(--bg-secondary) animate-pulse" /> 92 + </div> 93 + </TableCell> 94 + </TableRow> 95 + ); 96 + } 97 + 98 + interface PokemonTableContainerProps { 99 + /** 100 + * The actual PokemonTable component with data 101 + */ 102 + children: ReactNode; 103 + /** 104 + * Number of rows expected (for skeleton sizing) 105 + */ 106 + rowCount?: number; 107 + className?: string; 108 + } 109 + 110 + /** 111 + * Suspense-wrapped container for Pokemon tables. 112 + * 113 + * Provides a consistent loading state across all routes while 114 + * maintaining the console aesthetic. The table header is shown 115 + * immediately to set expectations for the data structure. 116 + * 117 + * @example 118 + * <PokemonTableContainer rowCount={10}> 119 + * <PokemonTable pokemon={data.pokemon} /> 120 + * </PokemonTableContainer> 121 + */ 122 + export function PokemonTableContainer({ 123 + children, 124 + rowCount, 125 + className, 126 + }: PokemonTableContainerProps) { 127 + return ( 128 + <Suspense fallback={<PokemonTableSkeleton rowCount={rowCount} className={className} />}> 129 + {children} 130 + </Suspense> 131 + ); 132 + }
+37 -13
src/routes/basic.tsx
··· 1 + import { Suspense } from "react"; 1 2 import { createFileRoute } from "@tanstack/react-router"; 2 - import { useSuspenseQuery } from "@tanstack/react-query"; 3 + import { useQuery, useSuspenseQuery } from "@tanstack/react-query"; 3 4 import * as v from "valibot"; 4 5 import { PaginationNav } from "~/components/pagination-nav"; 5 6 import { ConsoleCard } from "~/components/console/console-card"; 6 7 import { SectionHeader } from "~/components/console/section-header"; 7 - import { PokemonTable } from "~/components/console/pokemon-table"; 8 + import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; 8 9 import { POKEMON_LIMIT, getPokemonListQueryFn, getPokemonListQueryKey } from "~/util/pokemon"; 10 + import { lazily } from "~/util/lazily"; 11 + 12 + const { PokemonTable } = lazily(() => import("~/components/console/pokemon-table")); 9 13 10 14 const searchParamsSchema = v.object({ 11 15 offset: v.optional(v.number(), 0), ··· 18 22 19 23 function RouteComponent() { 20 24 const { offset: currentOffset } = Route.useSearch(); 21 - 22 - const { data } = useSuspenseQuery({ 23 - queryKey: getPokemonListQueryKey("suspense", currentOffset), 24 - queryFn: getPokemonListQueryFn, 25 - }); 26 25 27 26 return ( 28 27 <main className="min-h-screen bg-(--bg-primary) p-6"> ··· 33 32 <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 34 33 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 35 34 </h1> 36 - <PokemonTable pokemon={data.pokemon} /> 37 - <PaginationNav 38 - prevOffset={data.prevOffset ?? undefined} 39 - nextOffset={data.nextOffset ?? undefined} 40 - to="/basic" 41 - /> 35 + <div className="min-h-[500px]"> 36 + <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 37 + <PokemonTableContent currentOffset={currentOffset} /> 38 + </Suspense> 39 + </div> 40 + <PaginationNavOutlet /> 42 41 </ConsoleCard> 43 42 </div> 44 43 </main> 45 44 ); 46 45 } 46 + 47 + function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 48 + const { data } = useSuspenseQuery({ 49 + queryKey: getPokemonListQueryKey("suspense", currentOffset), 50 + queryFn: getPokemonListQueryFn, 51 + }); 52 + 53 + return <PokemonTable pokemon={data.pokemon} />; 54 + } 55 + 56 + function PaginationNavOutlet() { 57 + const { offset: currentOffset } = Route.useSearch(); 58 + const { data, isPending } = useQuery({ 59 + queryKey: getPokemonListQueryKey("suspense", currentOffset), 60 + queryFn: getPokemonListQueryFn, 61 + }); 62 + 63 + return ( 64 + <PaginationNav 65 + prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 66 + nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 67 + to="/basic" 68 + /> 69 + ); 70 + }
+45 -23
src/routes/debounced-preload-filters.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 1 + import { Suspense, useCallback, useState } from "react"; 2 + import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 2 3 import { useDebouncedCallback } from "@tanstack/react-pacer"; 3 - import { queryOptions, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; 4 - import { useCallback, useState } from "react"; 4 + import { queryOptions, useQuery, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; 5 5 import * as v from "valibot"; 6 6 import { FilterForm, FilterSubmitContext } from "~/components/filter-form"; 7 7 import { PaginationNav } from "~/components/pagination-nav"; 8 8 import { ConsoleCard } from "~/components/console/console-card"; 9 9 import { SectionHeader } from "~/components/console/section-header"; 10 - import { PokemonTable } from "~/components/console/pokemon-table"; 10 + import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; 11 11 import { 12 12 POKEMON_LIMIT, 13 13 getFilteredPokemonListQueryKey, 14 14 getFilteredPokemonListQueryFn, 15 15 } from "~/util/pokemon"; 16 + import { lazily } from "~/util/lazily"; 17 + 18 + const { PokemonTable } = lazily(() => import("~/components/console/pokemon-table")); 16 19 17 20 const searchParamsSchema = v.object({ 18 21 offset: v.optional(v.number(), 0), ··· 94 97 function RouteComponent() { 95 98 const { offset: currentOffset, name: nameFilter } = Route.useSearch(); 96 99 const navigate = Route.useNavigate(); 97 - const { pokemonListOptions } = Route.useRouteContext(); 98 - 99 - const { data } = useSuspenseQuery(pokemonListOptions); 100 - 101 - const filteredPokemon = data.pokemon; 102 100 103 101 return ( 104 102 <main className="min-h-screen bg-(--bg-primary) p-6"> ··· 133 131 )} 134 132 </h1> 135 133 136 - <PokemonTable pokemon={filteredPokemon} /> 137 - 138 - {filteredPokemon.length === 0 && nameFilter && ( 139 - <div className="text-center py-8 text-(--text-muted) font-mono"> 140 - No Pokemon found matching &quot;{nameFilter}&quot; 141 - </div> 142 - )} 143 - 144 - <PaginationNav 145 - prefetch="viewport" 146 - prevOffset={data.prevOffset ?? undefined} 147 - nextOffset={data.nextOffset ?? undefined} 148 - to="/debounced-preload-filters" 149 - /> 134 + <div className="min-h-[500px]"> 135 + <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 136 + <PokemonTableContent nameFilter={nameFilter} /> 137 + </Suspense> 138 + </div> 139 + <PaginationNavOutlet /> 150 140 </ConsoleCard> 151 141 </div> 152 142 </main> 153 143 ); 154 144 } 145 + 146 + function PokemonTableContent({ nameFilter }: { nameFilter: string }) { 147 + const { pokemonListOptions } = useRouteContext({ from: "/debounced-preload-filters" }); 148 + const { data } = useSuspenseQuery(pokemonListOptions); 149 + const filteredPokemon = data.pokemon; 150 + 151 + return ( 152 + <> 153 + <PokemonTable pokemon={filteredPokemon} /> 154 + 155 + {filteredPokemon.length === 0 && nameFilter && ( 156 + <div className="text-center py-4 text-(--text-muted) font-mono text-sm"> 157 + No Pokémon found matching &quot;{nameFilter}&quot; 158 + </div> 159 + )} 160 + </> 161 + ); 162 + } 163 + 164 + function PaginationNavOutlet() { 165 + const { pokemonListOptions } = useRouteContext({ from: "/debounced-preload-filters" }); 166 + const { data, isPending } = useQuery(pokemonListOptions); 167 + 168 + return ( 169 + <PaginationNav 170 + prefetch="viewport" 171 + prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 172 + nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 173 + to="/debounced-preload-filters" 174 + /> 175 + ); 176 + }
+45 -23
src/routes/filters.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 2 - import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 3 - import { useCallback, useState } from "react"; 1 + import { Suspense, useCallback, useState } from "react"; 2 + import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 + import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 5 import { FilterForm, FilterSubmitContext } from "~/components/filter-form"; 6 6 import { PaginationNav } from "~/components/pagination-nav"; 7 7 import { ConsoleCard } from "~/components/console/console-card"; 8 8 import { SectionHeader } from "~/components/console/section-header"; 9 - import { PokemonTable } from "~/components/console/pokemon-table"; 9 + import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; 10 10 import { 11 11 POKEMON_LIMIT, 12 12 getFilteredPokemonListQueryKey, 13 13 getFilteredPokemonListQueryFn, 14 14 } from "~/util/pokemon"; 15 + import { lazily } from "~/util/lazily"; 16 + 17 + const { PokemonTable } = lazily(() => import("~/components/console/pokemon-table")); 15 18 16 19 const searchParamsSchema = v.object({ 17 20 offset: v.optional(v.number(), 0), ··· 65 68 function RouteComponent() { 66 69 const { offset: currentOffset, name: nameFilter } = Route.useSearch(); 67 70 const navigate = Route.useNavigate(); 68 - const { pokemonListOptions } = Route.useRouteContext(); 69 - 70 - const { data } = useSuspenseQuery(pokemonListOptions); 71 - 72 - const filteredPokemon = data.pokemon; 73 71 74 72 return ( 75 73 <main className="min-h-screen bg-(--bg-primary) p-6"> ··· 102 100 )} 103 101 </h1> 104 102 105 - <PokemonTable pokemon={filteredPokemon} /> 106 - 107 - {filteredPokemon.length === 0 && nameFilter && ( 108 - <div className="text-center py-8 text-(--text-muted) font-mono"> 109 - No Pokemon found matching &quot;{nameFilter}&quot; 110 - </div> 111 - )} 112 - 113 - <PaginationNav 114 - prefetch="viewport" 115 - prevOffset={data.prevOffset ?? undefined} 116 - nextOffset={data.nextOffset ?? undefined} 117 - to="/filters" 118 - /> 103 + <div className="min-h-[500px]"> 104 + <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 105 + <PokemonTableContent nameFilter={nameFilter} /> 106 + </Suspense> 107 + </div> 108 + <PaginationNavOutlet /> 119 109 </ConsoleCard> 120 110 </div> 121 111 </main> 122 112 ); 123 113 } 114 + 115 + function PokemonTableContent({ nameFilter }: { nameFilter: string }) { 116 + const { pokemonListOptions } = useRouteContext({ from: "/filters" }); 117 + const { data } = useSuspenseQuery(pokemonListOptions); 118 + const filteredPokemon = data.pokemon; 119 + 120 + return ( 121 + <> 122 + <PokemonTable pokemon={filteredPokemon} /> 123 + 124 + {filteredPokemon.length === 0 && nameFilter && ( 125 + <div className="text-center py-4 text-(--text-muted) font-mono text-sm"> 126 + No Pokémon found matching &quot;{nameFilter}&quot; 127 + </div> 128 + )} 129 + </> 130 + ); 131 + } 132 + 133 + function PaginationNavOutlet() { 134 + const { pokemonListOptions } = useRouteContext({ from: "/filters" }); 135 + const { data, isPending } = useQuery(pokemonListOptions); 136 + 137 + return ( 138 + <PaginationNav 139 + prefetch="viewport" 140 + prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 141 + nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 142 + to="/filters" 143 + /> 144 + ); 145 + }
+34 -13
src/routes/intent-preloading.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 2 - import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 1 + import { Suspense } from "react"; 2 + import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 + import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query"; 3 4 import * as v from "valibot"; 4 5 import { PaginationNav } from "~/components/pagination-nav"; 5 6 import { ConsoleCard } from "~/components/console/console-card"; 6 7 import { SectionHeader } from "~/components/console/section-header"; 7 - import { PokemonTable } from "~/components/console/pokemon-table"; 8 + import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; 8 9 import { POKEMON_LIMIT, getPokemonListQueryKey, getPokemonListQueryFn } from "~/util/pokemon"; 10 + import { lazily } from "~/util/lazily"; 11 + 12 + const { PokemonTable } = lazily(() => import("~/components/console/pokemon-table")); 9 13 10 14 const searchParamsSchema = v.object({ 11 15 offset: v.optional(v.number(), 0), ··· 36 40 37 41 function RouteComponent() { 38 42 const { offset: currentOffset } = Route.useSearch(); 39 - const { pokemonListOptions } = Route.useRouteContext(); 40 - 41 - const { data } = useSuspenseQuery(pokemonListOptions); 42 43 43 44 return ( 44 45 <main className="min-h-screen bg-(--bg-primary) p-6"> ··· 49 50 <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 50 51 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 51 52 </h1> 52 - <PokemonTable pokemon={data.pokemon} /> 53 - <PaginationNav 54 - prefetch="intent" 55 - prevOffset={data.prevOffset ?? undefined} 56 - nextOffset={data.nextOffset ?? undefined} 57 - to="/intent-preloading" 58 - /> 53 + <div className="min-h-[500px]"> 54 + <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 55 + <PokemonTableContent /> 56 + </Suspense> 57 + </div> 58 + <PaginationNavOutlet /> 59 59 </ConsoleCard> 60 60 </div> 61 61 </main> 62 62 ); 63 63 } 64 + 65 + function PokemonTableContent() { 66 + const { pokemonListOptions } = useRouteContext({ from: "/intent-preloading" }); 67 + const { data } = useSuspenseQuery(pokemonListOptions); 68 + 69 + return <PokemonTable pokemon={data.pokemon} />; 70 + } 71 + 72 + function PaginationNavOutlet() { 73 + const { pokemonListOptions } = useRouteContext({ from: "/intent-preloading" }); 74 + const { data, isPending } = useQuery(pokemonListOptions); 75 + 76 + return ( 77 + <PaginationNav 78 + prefetch="intent" 79 + prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 80 + nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 81 + to="/intent-preloading" 82 + /> 83 + ); 84 + }
+34 -13
src/routes/pagination.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 2 - import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 1 + import { Suspense } from "react"; 2 + import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 + import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query"; 3 4 import * as v from "valibot"; 4 5 import { PaginationNav } from "~/components/pagination-nav"; 5 6 import { ConsoleCard } from "~/components/console/console-card"; 6 7 import { SectionHeader } from "~/components/console/section-header"; 7 - import { PokemonTable } from "~/components/console/pokemon-table"; 8 + import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; 8 9 import { POKEMON_LIMIT, getPokemonListQueryKey, getPokemonListQueryFn } from "~/util/pokemon"; 10 + import { lazily } from "~/util/lazily"; 11 + 12 + const { PokemonTable } = lazily(() => import("~/components/console/pokemon-table")); 9 13 10 14 const searchParamsSchema = v.object({ 11 15 offset: v.optional(v.number(), 0), ··· 36 40 37 41 function RouteComponent() { 38 42 const { offset: currentOffset } = Route.useSearch(); 39 - const { pokemonListOptions } = Route.useRouteContext(); 40 - 41 - const { data } = useSuspenseQuery(pokemonListOptions); 42 43 43 44 return ( 44 45 <main className="min-h-screen bg-(--bg-primary) p-6"> ··· 49 50 <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 50 51 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 51 52 </h1> 52 - <PokemonTable pokemon={data.pokemon} /> 53 - <PaginationNav 54 - prefetch="viewport" 55 - prevOffset={data.prevOffset ?? undefined} 56 - nextOffset={data.nextOffset ?? undefined} 57 - to="/pagination" 58 - /> 53 + <div className="min-h-[500px]"> 54 + <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 55 + <PokemonTableContent /> 56 + </Suspense> 57 + </div> 58 + <PaginationNavOutlet /> 59 59 </ConsoleCard> 60 60 </div> 61 61 </main> 62 62 ); 63 63 } 64 + 65 + function PokemonTableContent() { 66 + const { pokemonListOptions } = useRouteContext({ from: "/pagination" }); 67 + const { data } = useSuspenseQuery(pokemonListOptions); 68 + 69 + return <PokemonTable pokemon={data.pokemon} />; 70 + } 71 + 72 + function PaginationNavOutlet() { 73 + const { pokemonListOptions } = useRouteContext({ from: "/pagination" }); 74 + const { data, isPending } = useQuery(pokemonListOptions); 75 + 76 + return ( 77 + <PaginationNav 78 + prefetch="viewport" 79 + prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 80 + nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 81 + to="/pagination" 82 + /> 83 + ); 84 + }
+33 -14
src/routes/preloading.tsx
··· 1 - import { createFileRoute } from "@tanstack/react-router"; 2 - import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 1 + import { Suspense } from "react"; 2 + import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 + import { queryOptions, useQuery, useSuspenseQuery } from "@tanstack/react-query"; 3 4 import * as v from "valibot"; 4 5 import { PaginationNav } from "~/components/pagination-nav"; 5 6 import { ConsoleCard } from "~/components/console/console-card"; 6 7 import { SectionHeader } from "~/components/console/section-header"; 7 - import { PokemonTable } from "~/components/console/pokemon-table"; 8 + import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; 8 9 import { POKEMON_LIMIT, getPokemonListQueryKey, getPokemonListQueryFn } from "~/util/pokemon"; 10 + import { lazily } from "~/util/lazily"; 11 + 12 + const { PokemonTable } = lazily(() => import("~/components/console/pokemon-table")); 9 13 10 14 const searchParamsSchema = v.object({ 11 15 offset: v.optional(v.number(), 0), ··· 36 40 37 41 function RouteComponent() { 38 42 const { offset: currentOffset } = Route.useSearch(); 39 - const { pokemonListOptions: serverPokemonListOptions } = Route.useRouteContext(); 40 - 41 - const { data } = useSuspenseQuery({ 42 - ...serverPokemonListOptions, 43 - }); 44 43 45 44 return ( 46 45 <main className="min-h-screen bg-(--bg-primary) p-6"> ··· 51 50 <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 52 51 National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 53 52 </h1> 54 - <PokemonTable pokemon={data.pokemon} /> 55 - <PaginationNav 56 - prevOffset={data.prevOffset ?? undefined} 57 - nextOffset={data.nextOffset ?? undefined} 58 - to="/preloading" 59 - /> 53 + <div className="min-h-[500px]"> 54 + <Suspense fallback={<PokemonTableSkeleton rowCount={POKEMON_LIMIT} />}> 55 + <PokemonTableContent /> 56 + </Suspense> 57 + </div> 58 + <PaginationNavOutlet /> 60 59 </ConsoleCard> 61 60 </div> 62 61 </main> 63 62 ); 64 63 } 64 + 65 + function PokemonTableContent() { 66 + const { pokemonListOptions } = useRouteContext({ from: "/preloading" }); 67 + const { data } = useSuspenseQuery(pokemonListOptions); 68 + 69 + return <PokemonTable pokemon={data.pokemon} />; 70 + } 71 + 72 + function PaginationNavOutlet() { 73 + const { pokemonListOptions } = useRouteContext({ from: "/preloading" }); 74 + const { data, isPending } = useQuery(pokemonListOptions); 75 + 76 + return ( 77 + <PaginationNav 78 + prevOffset={isPending ? undefined : (data?.prevOffset ?? undefined)} 79 + nextOffset={isPending ? undefined : (data?.nextOffset ?? undefined)} 80 + to="/preloading" 81 + /> 82 + ); 83 + }
+40
src/util/lazily.ts
··· 1 + /* 2 + * src: github.com/JLarky/react-lazily/blob/main/src/core/lazily.ts 3 + * 4 + * MIT License 5 + * Copyright (c) 2020 JLarky 6 + * 7 + * Permission is hereby granted, free of charge, to any person obtaining a copy 8 + * of this software and associated documentation files (the "Software"), to deal 9 + * in the Software without restriction, including without limitation the rights 10 + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 + * copies of the Software, and to permit persons to whom the Software is 12 + * furnished to do so, subject to the following conditions: 13 + * 14 + * The above copyright notice and this permission notice shall be included in all 15 + * copies or substantial portions of the Software. 16 + * 17 + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 + SOFTWARE. 24 + * 25 + */ 26 + 27 + import { lazy } from "react"; 28 + 29 + export const lazily = <T extends {}, U extends keyof T>(loader: (x?: string) => Promise<T>) => 30 + new Proxy({} as unknown as T, { 31 + get: (_target, componentName: string | symbol) => { 32 + if (typeof componentName === "string") { 33 + return lazy(() => 34 + loader(componentName).then((x) => ({ 35 + default: x[componentName as U] as any as React.ComponentType<any>, 36 + })), 37 + ); 38 + } 39 + }, 40 + });