this repo has no description
0
fork

Configure Feed

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

general update

+555 -531
+2 -2
AGENTS.md
··· 30 30 31 31 Before substantial work: 32 32 33 - - Skill check: run `npx @tanstack/intent@latest list`, or use skills already listed in context. 34 - - Skill guidance: if one local skill clearly matches the task, run `npx @tanstack/intent@latest load <package>#<skill>` and follow the returned `SKILL.md`. 33 + - Skill check: run `vpx @tanstack/intent@latest list`, or use skills already listed in context. 34 + - Skill guidance: if one local skill clearly matches the task, run `vpx @tanstack/intent@latest load <package>#<skill>` and follow the returned `SKILL.md`. 35 35 - Monorepos: when working across packages, run the skill check from the workspace root and prefer the local skill for the package being changed. 36 36 - Multiple matches: prefer the most specific local skill for the package or concern you are changing; load additional skills only when the task spans multiple packages or concerns. 37 37 <!-- intent-skills:end -->
+1
lorum-ipsum.txt
··· 1 + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
+2 -2
package.json
··· 28 28 "@tanstack/react-router-devtools": "1.166.13", 29 29 "@tanstack/react-router-with-query": "1.130.17", 30 30 "@tanstack/react-start": "1.167.42", 31 - "@vitejs/plugin-rsc": "^0.5.24", 31 + "@vitejs/plugin-rsc": "^0.5.25", 32 32 "class-variance-authority": "^0.7.1", 33 33 "clsx": "^2.1.1", 34 34 "drizzle-orm": "1.0.0-beta.22", ··· 42 42 "valibot": "^1.3.1" 43 43 }, 44 44 "devDependencies": { 45 - "@netlify/vite-plugin-tanstack-start": "^1.3.7", 45 + "@netlify/vite-plugin-tanstack-start": "^1.3.8", 46 46 "@types/node": "^25.6.0", 47 47 "@types/react": "^19.2.14", 48 48 "@types/react-dom": "^19.2.3",
+7 -7
pnpm-lock.yaml
··· 58 58 specifier: 1.167.42 59 59 version: 1.167.42(@vitejs/plugin-rsc@0.5.25(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) 60 60 '@vitejs/plugin-rsc': 61 - specifier: ^0.5.24 61 + specifier: ^0.5.25 62 62 version: 0.5.25(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5) 63 63 class-variance-authority: 64 64 specifier: ^0.7.1 ··· 95 95 version: 1.3.1(typescript@6.0.3) 96 96 devDependencies: 97 97 '@netlify/vite-plugin-tanstack-start': 98 - specifier: ^1.3.7 98 + specifier: ^1.3.8 99 99 version: 1.3.8(@tanstack/react-start@1.167.42(@vitejs/plugin-rsc@0.5.25(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(crossws@0.4.5(srvx@0.11.15))(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3))(srvx@0.11.15) 100 100 '@types/node': 101 101 specifier: ^25.6.0 ··· 122 122 specifier: ^6.0.3 123 123 version: 6.0.3 124 124 vite: 125 - specifier: npm:@voidzero-dev/vite-plus-core@latest 125 + specifier: npm:@voidzero-dev/vite-plus-core@^0.1.19 126 126 version: '@voidzero-dev/vite-plus-core@0.1.19(@types/node@25.6.0)(esbuild@0.27.7)(jiti@2.6.1)(tsx@4.21.0)(typescript@6.0.3)(yaml@2.8.3)' 127 127 vite-plus: 128 128 specifier: latest ··· 2708 2708 bare-path@3.0.0: 2709 2709 resolution: {integrity: sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==} 2710 2710 2711 - bare-stream@2.13.0: 2712 - resolution: {integrity: sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==} 2711 + bare-stream@2.13.1: 2712 + resolution: {integrity: sha512-Vp0cnjYyrEC4whYTymQ+YZi6pBpfiICZO3cfRG8sy67ZNWe951urv1x4eW1BKNngw3U+3fPYb5JQvHbCtxH7Ow==} 2713 2713 peerDependencies: 2714 2714 bare-abort-controller: '*' 2715 2715 bare-buffer: '*' ··· 7520 7520 dependencies: 7521 7521 bare-events: 2.8.2 7522 7522 bare-path: 3.0.0 7523 - bare-stream: 2.13.0(bare-events@2.8.2) 7523 + bare-stream: 2.13.1(bare-events@2.8.2) 7524 7524 bare-url: 2.4.2 7525 7525 fast-fifo: 1.3.2 7526 7526 transitivePeerDependencies: ··· 7533 7533 dependencies: 7534 7534 bare-os: 3.9.0 7535 7535 7536 - bare-stream@2.13.0(bare-events@2.8.2): 7536 + bare-stream@2.13.1(bare-events@2.8.2): 7537 7537 dependencies: 7538 7538 streamx: 2.25.0 7539 7539 teex: 1.0.1
+1 -1
src/components/console/pokemon-table.tsx
··· 59 59 "uppercase text-xs tracking-wider", 60 60 )} 61 61 > 62 - Details 62 + Types 63 63 </TableHead> 64 64 </TableRow> 65 65 </TableHeader>
-56
src/components/console/query-trace-utils.ts
··· 1 - import type { QueryStatus } from "@tanstack/react-query"; 2 - 3 - type FetchStatus = "fetching" | "idle" | "paused"; 4 - 5 - export function getCacheStatus(dataUpdatedAt?: number) { 6 - return dataUpdatedAt 7 - ? { indicator: "cached" as const, label: "cache populated" } 8 - : { indicator: "idle" as const, label: "cache empty" }; 9 - } 10 - 11 - export function getFetchStatus(fetchStatus?: FetchStatus, queryStatus?: QueryStatus) { 12 - if (queryStatus === "error") { 13 - return { indicator: "error" as const, label: "error" }; 14 - } 15 - 16 - if (fetchStatus === "fetching") { 17 - return { indicator: "fetching" as const, label: "fetching" }; 18 - } 19 - 20 - if (fetchStatus === "paused") { 21 - return { indicator: "idle" as const, label: "paused" }; 22 - } 23 - 24 - return { indicator: "idle" as const, label: "request idle" }; 25 - } 26 - 27 - export function getLoadingCacheStatus() { 28 - return { indicator: "idle" as const, label: "cache empty" }; 29 - } 30 - 31 - export function getLoadingFetchStatus() { 32 - return { indicator: "fetching" as const, label: "fetching" }; 33 - } 34 - 35 - export function getPreloadStatus(dataUpdatedAt?: number) { 36 - return dataUpdatedAt 37 - ? { indicator: "cached" as const, label: "preload complete" } 38 - : { indicator: "idle" as const, label: "not preloaded" }; 39 - } 40 - 41 - export function getLoadingPreloadStatus() { 42 - return { indicator: "fetching" as const, label: "preloading" }; 43 - } 44 - 45 - export function formatPokemonListQueryKey(location: string, offset: number) { 46 - return `pokemon-list / ${location} / offset ${offset}`; 47 - } 48 - 49 - export function formatFilteredPokemonListQueryKey( 50 - location: string, 51 - offset: number, 52 - nameFilter: string, 53 - ) { 54 - const filter = nameFilter ? `name "${nameFilter}"` : 'name ""'; 55 - return `pokemon-list / ${location} / offset ${offset} / ${filter}`; 56 - }
-77
src/components/console/query-trace.tsx
··· 1 - import type { ReactNode } from "react"; 2 - import { StatusDotWithLabel } from "./status-dot"; 3 - 4 - type QueryTraceIndicator = "cached" | "fetching" | "idle" | "error"; 5 - 6 - interface QueryTraceStatus { 7 - indicator: QueryTraceIndicator; 8 - label: string; 9 - } 10 - 11 - interface QueryTraceProps { 12 - behaviorDescription: ReactNode; 13 - cacheStatus: QueryTraceStatus; 14 - fetchStatus: QueryTraceStatus; 15 - preloadStatus?: QueryTraceStatus; 16 - queryKeys: ReactNode[]; 17 - strategyDescription: ReactNode; 18 - } 19 - 20 - export function QueryTrace({ 21 - behaviorDescription, 22 - cacheStatus, 23 - fetchStatus, 24 - preloadStatus, 25 - queryKeys, 26 - strategyDescription, 27 - }: QueryTraceProps) { 28 - return ( 29 - <section 30 - className="mb-5 border border-(--border-default) bg-(--bg-secondary) p-4 font-mono" 31 - aria-label="Query behavior" 32 - > 33 - <div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"> 34 - <div className="max-w-2xl"> 35 - <p className="text-xs font-semibold uppercase tracking-wider text-(--text-secondary)"> 36 - Behavior 37 - </p> 38 - <p className="mt-1 text-sm leading-relaxed text-(--text-primary)"> 39 - {behaviorDescription} 40 - </p> 41 - </div> 42 - <div className="flex flex-wrap gap-3 text-xs"> 43 - <StatusDotWithLabel status={cacheStatus.indicator} label={cacheStatus.label} /> 44 - {preloadStatus && ( 45 - <StatusDotWithLabel status={preloadStatus.indicator} label={preloadStatus.label} /> 46 - )} 47 - <StatusDotWithLabel status={fetchStatus.indicator} label={fetchStatus.label} /> 48 - </div> 49 - </div> 50 - 51 - <dl className="mt-4 grid gap-3 border-t border-(--border-default) pt-3 text-xs sm:grid-cols-4"> 52 - <div> 53 - <dt className="mb-1 uppercase tracking-wider text-(--text-muted)">Strategy</dt> 54 - <dd className="text-(--text-primary)">{strategyDescription}</dd> 55 - </div> 56 - <div> 57 - <dt className="mb-1 uppercase tracking-wider text-(--text-muted)">Query keys</dt> 58 - <dd className="space-y-1 text-(--text-primary)"> 59 - {queryKeys.map((queryKey, index) => ( 60 - <div key={index}>{queryKey}</div> 61 - ))} 62 - </dd> 63 - </div> 64 - <div> 65 - <dt className="mb-1 uppercase tracking-wider text-(--text-muted)">Fetch status</dt> 66 - <dd className="text-(--text-primary)">{fetchStatus.label}</dd> 67 - </div> 68 - {preloadStatus && ( 69 - <div> 70 - <dt className="mb-1 uppercase tracking-wider text-(--text-muted)">Preload status</dt> 71 - <dd className="text-(--text-primary)">{preloadStatus.label}</dd> 72 - </div> 73 - )} 74 - </dl> 75 - </section> 76 - ); 77 - }
+2
src/components/header.tsx
··· 9 9 { to: "/pagination", label: "04_pagination", preload: "intent" as const }, 10 10 { to: "/filters", label: "05_filters", preload: "intent" as const }, 11 11 { to: "/debounced-preload-filters", label: "06_debounced-filters", preload: "intent" as const }, 12 + { to: "/live-query", label: "07_live-query", preload: "intent" as const }, 13 + { to: "/live-query-filters", label: "08_live-query-filters", preload: "intent" as const }, 12 14 ]; 13 15 14 16 export function Header() {
+27
src/components/strategy-article.tsx
··· 1 + interface StrategyArticleProps { 2 + eyebrow: string; 3 + title: string; 4 + } 5 + 6 + const loremIpsum = 7 + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."; 8 + 9 + export function StrategyArticle({ eyebrow, title }: StrategyArticleProps) { 10 + return ( 11 + <article className="sticky top-6 rounded-[2rem] border border-(--border-default) bg-(--bg-secondary) p-6 shadow-[8px_8px_0_var(--border-default)] md:p-8"> 12 + <p className="mb-5 font-mono text-xs uppercase tracking-[0.3em] text-(--accent-default)"> 13 + {eyebrow} 14 + </p> 15 + <h2 className="mb-6 text-4xl font-black uppercase leading-none tracking-tight text-(--text-primary) md:text-5xl"> 16 + {title} 17 + </h2> 18 + <div className="space-y-5 font-serif text-lg leading-8 text-(--text-secondary)"> 19 + <p>{loremIpsum}</p> 20 + <p>{loremIpsum}</p> 21 + </div> 22 + <div className="mt-8 border-t border-(--border-default) pt-5 font-mono text-xs uppercase tracking-widest text-(--text-muted)"> 23 + Strategy notes / walkthrough 24 + </div> 25 + </article> 26 + ); 27 + }
+20
src/components/strategy-page-layout.tsx
··· 1 + import { StrategyArticle } from "~/components/strategy-article"; 2 + 3 + interface StrategyPageLayoutProps { 4 + articleEyebrow: string; 5 + articleTitle: string; 6 + children: React.ReactNode; 7 + } 8 + 9 + export function StrategyPageLayout({ 10 + articleEyebrow, 11 + articleTitle, 12 + children, 13 + }: StrategyPageLayoutProps) { 14 + return ( 15 + <div className="grid gap-8 xl:grid-cols-[minmax(0,0.9fr)_minmax(34rem,1.1fr)] xl:items-start"> 16 + <StrategyArticle eyebrow={articleEyebrow} title={articleTitle} /> 17 + <div className="min-w-0">{children}</div> 18 + </div> 19 + ); 20 + }
+4
src/lib/collections.ts
··· 1 1 import { BasicIndex, createCollection } from "@tanstack/react-db"; 2 2 import { electricCollectionOptions } from "@tanstack/electric-db-collection"; 3 + import { snakeCamelMapper } from "@electric-sql/client"; 3 4 4 5 import { pokemonSelectSchema, typesSelectSchema, pokemonTypesSelectSchema } from "~/data/schema"; 5 6 ··· 7 8 electricCollectionOptions({ 8 9 shapeOptions: { 9 10 url: "http://localhost:3000/api/shapes/pokemon", 11 + columnMapper: snakeCamelMapper(), 10 12 }, 11 13 getKey: (item) => item.id, 12 14 schema: pokemonSelectSchema, ··· 19 21 electricCollectionOptions({ 20 22 shapeOptions: { 21 23 url: "http://localhost:3000/api/shapes/types", 24 + columnMapper: snakeCamelMapper(), 22 25 }, 23 26 getKey: (item) => item.id, 24 27 schema: typesSelectSchema, ··· 30 33 electricCollectionOptions({ 31 34 shapeOptions: { 32 35 url: "http://localhost:3000/api/shapes/pokemon-types", 36 + columnMapper: snakeCamelMapper(), 33 37 }, 34 38 getKey: (item) => item.id, 35 39 schema: pokemonTypesSelectSchema,
+21
src/routeTree.gen.ts
··· 11 11 import { Route as rootRouteImport } from './routes/__root' 12 12 import { Route as PreloadingRouteImport } from './routes/preloading' 13 13 import { Route as PaginationRouteImport } from './routes/pagination' 14 + import { Route as LiveQueryFiltersRouteImport } from './routes/live-query-filters' 14 15 import { Route as LiveQueryRouteImport } from './routes/live-query' 15 16 import { Route as IntentPreloadingRouteImport } from './routes/intent-preloading' 16 17 import { Route as FiltersRouteImport } from './routes/filters' ··· 29 30 const PaginationRoute = PaginationRouteImport.update({ 30 31 id: '/pagination', 31 32 path: '/pagination', 33 + getParentRoute: () => rootRouteImport, 34 + } as any) 35 + const LiveQueryFiltersRoute = LiveQueryFiltersRouteImport.update({ 36 + id: '/live-query-filters', 37 + path: '/live-query-filters', 32 38 getParentRoute: () => rootRouteImport, 33 39 } as any) 34 40 const LiveQueryRoute = LiveQueryRouteImport.update({ ··· 84 90 '/filters': typeof FiltersRoute 85 91 '/intent-preloading': typeof IntentPreloadingRoute 86 92 '/live-query': typeof LiveQueryRoute 93 + '/live-query-filters': typeof LiveQueryFiltersRoute 87 94 '/pagination': typeof PaginationRoute 88 95 '/preloading': typeof PreloadingRoute 89 96 '/api/shapes/pokemon': typeof ApiShapesPokemonRoute ··· 97 104 '/filters': typeof FiltersRoute 98 105 '/intent-preloading': typeof IntentPreloadingRoute 99 106 '/live-query': typeof LiveQueryRoute 107 + '/live-query-filters': typeof LiveQueryFiltersRoute 100 108 '/pagination': typeof PaginationRoute 101 109 '/preloading': typeof PreloadingRoute 102 110 '/api/shapes/pokemon': typeof ApiShapesPokemonRoute ··· 111 119 '/filters': typeof FiltersRoute 112 120 '/intent-preloading': typeof IntentPreloadingRoute 113 121 '/live-query': typeof LiveQueryRoute 122 + '/live-query-filters': typeof LiveQueryFiltersRoute 114 123 '/pagination': typeof PaginationRoute 115 124 '/preloading': typeof PreloadingRoute 116 125 '/api/shapes/pokemon': typeof ApiShapesPokemonRoute ··· 126 135 | '/filters' 127 136 | '/intent-preloading' 128 137 | '/live-query' 138 + | '/live-query-filters' 129 139 | '/pagination' 130 140 | '/preloading' 131 141 | '/api/shapes/pokemon' ··· 139 149 | '/filters' 140 150 | '/intent-preloading' 141 151 | '/live-query' 152 + | '/live-query-filters' 142 153 | '/pagination' 143 154 | '/preloading' 144 155 | '/api/shapes/pokemon' ··· 152 163 | '/filters' 153 164 | '/intent-preloading' 154 165 | '/live-query' 166 + | '/live-query-filters' 155 167 | '/pagination' 156 168 | '/preloading' 157 169 | '/api/shapes/pokemon' ··· 166 178 FiltersRoute: typeof FiltersRoute 167 179 IntentPreloadingRoute: typeof IntentPreloadingRoute 168 180 LiveQueryRoute: typeof LiveQueryRoute 181 + LiveQueryFiltersRoute: typeof LiveQueryFiltersRoute 169 182 PaginationRoute: typeof PaginationRoute 170 183 PreloadingRoute: typeof PreloadingRoute 171 184 ApiShapesPokemonRoute: typeof ApiShapesPokemonRoute ··· 187 200 path: '/pagination' 188 201 fullPath: '/pagination' 189 202 preLoaderRoute: typeof PaginationRouteImport 203 + parentRoute: typeof rootRouteImport 204 + } 205 + '/live-query-filters': { 206 + id: '/live-query-filters' 207 + path: '/live-query-filters' 208 + fullPath: '/live-query-filters' 209 + preLoaderRoute: typeof LiveQueryFiltersRouteImport 190 210 parentRoute: typeof rootRouteImport 191 211 } 192 212 '/live-query': { ··· 262 282 FiltersRoute: FiltersRoute, 263 283 IntentPreloadingRoute: IntentPreloadingRoute, 264 284 LiveQueryRoute: LiveQueryRoute, 285 + LiveQueryFiltersRoute: LiveQueryFiltersRoute, 265 286 PaginationRoute: PaginationRoute, 266 287 PreloadingRoute: PreloadingRoute, 267 288 ApiShapesPokemonRoute: ApiShapesPokemonRoute,
+22 -44
src/routes/basic.tsx
··· 2 2 import { createFileRoute } from "@tanstack/react-router"; 3 3 import { useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 - import { QueryTrace } from "~/components/console/query-trace"; 6 - import { 7 - formatPokemonListQueryKey, 8 - getCacheStatus, 9 - getFetchStatus, 10 - getLoadingCacheStatus, 11 - getLoadingFetchStatus, 12 - } from "~/components/console/query-trace-utils"; 13 5 import { PaginationNav } from "~/components/pagination-nav"; 6 + import { StrategyPageLayout } from "~/components/strategy-page-layout"; 14 7 import { ConsoleCard } from "~/components/console/console-card"; 15 8 import { SectionHeader } from "~/components/console/section-header"; 16 9 import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; ··· 24 17 offset: v.optional(v.number(), 0), 25 18 }); 26 19 27 - const getBasicQueryTraceProps = (currentOffset: number) => ({ 28 - behaviorDescription: 29 - "No preload: the Pokémon query starts after this route renders. Until it resolves, the table shows its loading state.", 30 - queryKeys: [formatPokemonListQueryKey("suspense", currentOffset)], 31 - strategyDescription: "none", 32 - }); 33 - 34 20 export const Route = createFileRoute("/basic")({ 35 21 validateSearch: searchParamsSchema, 36 22 component: RouteComponent, ··· 41 27 42 28 return ( 43 29 <main className="min-h-screen bg-(--bg-primary) p-6"> 44 - <div className="max-w-4xl mx-auto"> 30 + <div className="max-w-7xl mx-auto"> 45 31 <SectionHeader title="01_basic" subtitle="// No prefetching (baseline)" /> 46 32 47 - <ConsoleCard className="mb-6"> 48 - <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 49 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 50 - </h1> 51 - <Suspense 52 - fallback={ 53 - <> 54 - <PokemonTableShell> 55 - <QueryTrace 56 - {...getBasicQueryTraceProps(currentOffset)} 57 - cacheStatus={getLoadingCacheStatus()} 58 - fetchStatus={getLoadingFetchStatus()} 59 - /> 60 - <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 61 - </PokemonTableShell> 62 - <PaginationNav prevOffset={null} nextOffset={null} to="/basic" /> 63 - </> 64 - } 65 - > 66 - <PokemonTableContent currentOffset={currentOffset} /> 67 - </Suspense> 68 - </ConsoleCard> 33 + <StrategyPageLayout articleEyebrow="Baseline" articleTitle="No prefetching"> 34 + <ConsoleCard className="mb-6"> 35 + <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 36 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 37 + </h1> 38 + <Suspense 39 + fallback={ 40 + <> 41 + <PokemonTableShell> 42 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 43 + </PokemonTableShell> 44 + <PaginationNav prevOffset={null} nextOffset={null} to="/basic" /> 45 + </> 46 + } 47 + > 48 + <PokemonTableContent currentOffset={currentOffset} /> 49 + </Suspense> 50 + </ConsoleCard> 51 + </StrategyPageLayout> 69 52 </div> 70 53 </main> 71 54 ); ··· 77 60 78 61 function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 79 62 const queryKey = getPokemonListQueryKey("suspense", currentOffset); 80 - const { data, dataUpdatedAt, fetchStatus, isFetching } = useSuspenseQuery({ 63 + const { data } = useSuspenseQuery({ 81 64 queryKey, 82 65 queryFn: getPokemonListQueryFn, 83 66 }); ··· 85 68 return ( 86 69 <> 87 70 <PokemonTableShell> 88 - <QueryTrace 89 - {...getBasicQueryTraceProps(currentOffset)} 90 - cacheStatus={getCacheStatus(dataUpdatedAt)} 91 - fetchStatus={getFetchStatus(fetchStatus, isFetching ? "pending" : "success")} 92 - /> 93 71 <PokemonTable pokemon={data.pokemon} /> 94 72 </PokemonTableShell> 95 73 <PaginationNav prevOffset={data.prevOffset} nextOffset={data.nextOffset} to="/basic" />
+51 -82
src/routes/debounced-preload-filters.tsx
··· 3 3 import { useDebouncedCallback } from "@tanstack/react-pacer"; 4 4 import { queryOptions, useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; 5 5 import * as v from "valibot"; 6 - import { QueryTrace } from "~/components/console/query-trace"; 7 - import { 8 - formatFilteredPokemonListQueryKey, 9 - getCacheStatus, 10 - getFetchStatus, 11 - getLoadingCacheStatus, 12 - getLoadingFetchStatus, 13 - getLoadingPreloadStatus, 14 - getPreloadStatus, 15 - } from "~/components/console/query-trace-utils"; 16 6 import { FilterForm, FilterSubmitContext } from "~/components/filter-form"; 17 7 import { PaginationNav } from "~/components/pagination-nav"; 8 + import { StrategyPageLayout } from "~/components/strategy-page-layout"; 18 9 import { ConsoleCard } from "~/components/console/console-card"; 19 10 import { SectionHeader } from "~/components/console/section-header"; 20 11 import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; ··· 27 18 const searchParamsSchema = v.object({ 28 19 offset: v.optional(v.number(), 0), 29 20 name: v.optional(v.string(), ""), 30 - }); 31 - 32 - const getDebouncedQueryTraceProps = (currentOffset: number, nameFilter: string) => ({ 33 - behaviorDescription: 34 - "Debounced preload: typing starts filtered Pokémon prefetches before submit, then the route loader confirms the submitted query.", 35 - queryKeys: [ 36 - formatFilteredPokemonListQueryKey("debounced-preload-filters", currentOffset, nameFilter), 37 - ], 38 - strategyDescription: "100ms debounced prefetchQuery + loader prefetchQuery", 39 21 }); 40 22 41 23 export const Route = createFileRoute("/debounced-preload-filters")({ ··· 116 98 117 99 return ( 118 100 <main className="min-h-screen bg-(--bg-primary) p-6"> 119 - <div className="max-w-4xl mx-auto"> 101 + <div className="max-w-7xl mx-auto"> 120 102 <SectionHeader title="06_debounced" subtitle="// Advanced filter prefetch" /> 121 103 122 - {/* Filter UI */} 123 - <ConsoleCard className="mb-6"> 124 - <h2 className="text-sm font-semibold mb-4 text-(--text-primary) uppercase tracking-wider"> 125 - Filters 126 - </h2> 127 - <p className="text-sm text-(--text-muted) mb-4"> 128 - Preloads results while typing (debounced 100ms) 129 - </p> 130 - <PreloadFilterSubmitContextProvider 131 - initialName={nameFilter} 132 - handleSubmit={(newNameFilter) => { 133 - void navigate({ 134 - search: { name: newNameFilter }, 135 - }); 136 - }} 137 - > 138 - <FilterForm /> 139 - </PreloadFilterSubmitContextProvider> 140 - </ConsoleCard> 104 + <StrategyPageLayout 105 + articleEyebrow="Debounced search" 106 + articleTitle="Debounced filter prefetch" 107 + > 108 + {/* Filter UI */} 109 + <ConsoleCard className="mb-6"> 110 + <h2 className="text-sm font-semibold mb-4 text-(--text-primary) uppercase tracking-wider"> 111 + Filters 112 + </h2> 113 + <p className="text-sm text-(--text-muted) mb-4"> 114 + Preloads results while typing (debounced 100ms) 115 + </p> 116 + <PreloadFilterSubmitContextProvider 117 + initialName={nameFilter} 118 + handleSubmit={(newNameFilter) => { 119 + void navigate({ 120 + search: { name: newNameFilter }, 121 + }); 122 + }} 123 + > 124 + <FilterForm /> 125 + </PreloadFilterSubmitContextProvider> 126 + </ConsoleCard> 141 127 142 - <ConsoleCard> 143 - <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 144 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 145 - {nameFilter && ( 146 - <span className="text-(--text-muted)"> (filtered: &quot;{nameFilter}&quot;)</span> 147 - )} 148 - </h1> 128 + <ConsoleCard> 129 + <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 130 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 131 + {nameFilter && ( 132 + <span className="text-(--text-muted)"> (filtered: &quot;{nameFilter}&quot;)</span> 133 + )} 134 + </h1> 149 135 150 - <Suspense 151 - fallback={ 152 - <> 153 - <div className="min-h-125"> 154 - <QueryTrace 155 - {...getDebouncedQueryTraceProps(currentOffset, nameFilter)} 156 - cacheStatus={getLoadingCacheStatus()} 157 - fetchStatus={getLoadingFetchStatus()} 158 - preloadStatus={getLoadingPreloadStatus()} 136 + <Suspense 137 + fallback={ 138 + <> 139 + <div className="min-h-125"> 140 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 141 + </div> 142 + <PaginationNav 143 + prevOffset={null} 144 + nextOffset={null} 145 + to="/debounced-preload-filters" 159 146 /> 160 - <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 161 - </div> 162 - <PaginationNav 163 - prevOffset={null} 164 - nextOffset={null} 165 - to="/debounced-preload-filters" 166 - /> 167 - </> 168 - } 169 - > 170 - <PokemonTableContent currentOffset={currentOffset} nameFilter={nameFilter} /> 171 - </Suspense> 172 - </ConsoleCard> 147 + </> 148 + } 149 + > 150 + <PokemonTableContent nameFilter={nameFilter} /> 151 + </Suspense> 152 + </ConsoleCard> 153 + </StrategyPageLayout> 173 154 </div> 174 155 </main> 175 156 ); 176 157 } 177 158 178 - function PokemonTableContent({ 179 - currentOffset, 180 - nameFilter, 181 - }: { 182 - currentOffset: number; 183 - nameFilter: string; 184 - }) { 159 + function PokemonTableContent({ nameFilter }: { nameFilter: string }) { 185 160 const { pokemonListOptions } = useRouteContext({ from: "/debounced-preload-filters" }); 186 - const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 161 + const { data } = useSuspenseQuery(pokemonListOptions); 187 162 const filteredPokemon = data.pokemon; 188 163 189 164 return ( 190 165 <> 191 166 <div className="min-h-125"> 192 - <QueryTrace 193 - {...getDebouncedQueryTraceProps(currentOffset, nameFilter)} 194 - cacheStatus={getCacheStatus(dataUpdatedAt)} 195 - fetchStatus={getFetchStatus(fetchStatus, status)} 196 - preloadStatus={getPreloadStatus(dataUpdatedAt)} 197 - /> 198 167 <PokemonTable pokemon={filteredPokemon} /> 199 168 200 169 {filteredPokemon.length === 0 && nameFilter && (
+43 -75
src/routes/filters.tsx
··· 2 2 import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 3 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 - import { QueryTrace } from "~/components/console/query-trace"; 6 - import { 7 - formatFilteredPokemonListQueryKey, 8 - getCacheStatus, 9 - getFetchStatus, 10 - getLoadingCacheStatus, 11 - getLoadingFetchStatus, 12 - getLoadingPreloadStatus, 13 - getPreloadStatus, 14 - } from "~/components/console/query-trace-utils"; 15 5 import { FilterForm, FilterSubmitContext } from "~/components/filter-form"; 16 6 import { PaginationNav } from "~/components/pagination-nav"; 7 + import { StrategyPageLayout } from "~/components/strategy-page-layout"; 17 8 import { ConsoleCard } from "~/components/console/console-card"; 18 9 import { SectionHeader } from "~/components/console/section-header"; 19 10 import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; ··· 26 17 const searchParamsSchema = v.object({ 27 18 offset: v.optional(v.number(), 0), 28 19 name: v.optional(v.string(), ""), 29 - }); 30 - 31 - const getFiltersQueryTraceProps = (currentOffset: number, nameFilter: string) => ({ 32 - behaviorDescription: 33 - "Submitted search preload: the route loader prefetches the filtered Pokémon query after the search params change.", 34 - queryKeys: [formatFilteredPokemonListQueryKey("filters", currentOffset, nameFilter)], 35 - strategyDescription: "filter submit updates search params + loader prefetchQuery", 36 20 }); 37 21 38 22 function FilterSubmitContextProvider(props: { ··· 85 69 86 70 return ( 87 71 <main className="min-h-screen bg-(--bg-primary) p-6"> 88 - <div className="max-w-4xl mx-auto"> 72 + <div className="max-w-7xl mx-auto"> 89 73 <SectionHeader title="05_filters" subtitle="// Search with prefetch" /> 90 74 91 - {/* Filter UI */} 92 - <ConsoleCard className="mb-6"> 93 - <h2 className="text-sm font-semibold mb-4 text-(--text-primary) uppercase tracking-wider"> 94 - Filters 95 - </h2> 96 - <FilterSubmitContextProvider 97 - key={`filter-submit-context-provider-${nameFilter}`} 98 - initialName={nameFilter} 99 - handleSubmit={(newNameFilter) => { 100 - void navigate({ 101 - search: { name: newNameFilter }, 102 - }); 103 - }} 104 - > 105 - <FilterForm /> 106 - </FilterSubmitContextProvider> 107 - </ConsoleCard> 75 + <StrategyPageLayout articleEyebrow="Search" articleTitle="Submitted filter prefetch"> 76 + {/* Filter UI */} 77 + <ConsoleCard className="mb-6"> 78 + <h2 className="text-sm font-semibold mb-4 text-(--text-primary) uppercase tracking-wider"> 79 + Filters 80 + </h2> 81 + <FilterSubmitContextProvider 82 + key={`filter-submit-context-provider-${nameFilter}`} 83 + initialName={nameFilter} 84 + handleSubmit={(newNameFilter) => { 85 + void navigate({ 86 + search: { name: newNameFilter }, 87 + }); 88 + }} 89 + > 90 + <FilterForm /> 91 + </FilterSubmitContextProvider> 92 + </ConsoleCard> 108 93 109 - <ConsoleCard> 110 - <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 111 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 112 - {nameFilter && ( 113 - <span className="text-(--text-muted)"> (filtered: &quot;{nameFilter}&quot;)</span> 114 - )} 115 - </h1> 94 + <ConsoleCard> 95 + <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 96 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 97 + {nameFilter && ( 98 + <span className="text-(--text-muted)"> (filtered: &quot;{nameFilter}&quot;)</span> 99 + )} 100 + </h1> 116 101 117 - <Suspense 118 - fallback={ 119 - <> 120 - <div className="min-h-125"> 121 - <QueryTrace 122 - {...getFiltersQueryTraceProps(currentOffset, nameFilter)} 123 - cacheStatus={getLoadingCacheStatus()} 124 - fetchStatus={getLoadingFetchStatus()} 125 - preloadStatus={getLoadingPreloadStatus()} 126 - /> 127 - <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 128 - </div> 129 - <PaginationNav prevOffset={null} nextOffset={null} to="/filters" /> 130 - </> 131 - } 132 - > 133 - <PokemonTableContent currentOffset={currentOffset} nameFilter={nameFilter} /> 134 - </Suspense> 135 - </ConsoleCard> 102 + <Suspense 103 + fallback={ 104 + <> 105 + <div className="min-h-125"> 106 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 107 + </div> 108 + <PaginationNav prevOffset={null} nextOffset={null} to="/filters" /> 109 + </> 110 + } 111 + > 112 + <PokemonTableContent nameFilter={nameFilter} /> 113 + </Suspense> 114 + </ConsoleCard> 115 + </StrategyPageLayout> 136 116 </div> 137 117 </main> 138 118 ); 139 119 } 140 120 141 - function PokemonTableContent({ 142 - currentOffset, 143 - nameFilter, 144 - }: { 145 - currentOffset: number; 146 - nameFilter: string; 147 - }) { 121 + function PokemonTableContent({ nameFilter }: { nameFilter: string }) { 148 122 const { pokemonListOptions } = useRouteContext({ from: "/filters" }); 149 - const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 123 + const { data } = useSuspenseQuery(pokemonListOptions); 150 124 const filteredPokemon = data.pokemon; 151 125 152 126 return ( 153 127 <> 154 128 <div className="min-h-125"> 155 - <QueryTrace 156 - {...getFiltersQueryTraceProps(currentOffset, nameFilter)} 157 - cacheStatus={getCacheStatus(dataUpdatedAt)} 158 - fetchStatus={getFetchStatus(fetchStatus, status)} 159 - preloadStatus={getPreloadStatus(dataUpdatedAt)} 160 - /> 161 129 <PokemonTable pokemon={filteredPokemon} /> 162 130 163 131 {filteredPokemon.length === 0 && nameFilter && (
+23 -49
src/routes/intent-preloading.tsx
··· 2 2 import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 3 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 - import { QueryTrace } from "~/components/console/query-trace"; 6 - import { 7 - formatPokemonListQueryKey, 8 - getCacheStatus, 9 - getFetchStatus, 10 - getLoadingCacheStatus, 11 - getLoadingFetchStatus, 12 - getLoadingPreloadStatus, 13 - getPreloadStatus, 14 - } from "~/components/console/query-trace-utils"; 15 5 import { PaginationNav } from "~/components/pagination-nav"; 6 + import { StrategyPageLayout } from "~/components/strategy-page-layout"; 16 7 import { ConsoleCard } from "~/components/console/console-card"; 17 8 import { SectionHeader } from "~/components/console/section-header"; 18 9 import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; ··· 24 15 25 16 const searchParamsSchema = v.object({ 26 17 offset: v.optional(v.number(), 0), 27 - }); 28 - 29 - const getIntentPreloadingQueryTraceProps = (currentOffset: number) => ({ 30 - behaviorDescription: 31 - "Intent preload: hovering a navigation target starts the Pokémon query before click, then the route loader ensures the active page is prefetched.", 32 - queryKeys: [formatPokemonListQueryKey("intent-preloading", currentOffset)], 33 - strategyDescription: 'link preload="intent" + loader prefetchQuery', 34 18 }); 35 19 36 20 export const Route = createFileRoute("/intent-preloading")({ ··· 61 45 62 46 return ( 63 47 <main className="min-h-screen bg-(--bg-primary) p-6"> 64 - <div className="max-w-4xl mx-auto"> 48 + <div className="max-w-7xl mx-auto"> 65 49 <SectionHeader title="03_intent-preloading" subtitle="// Hover-based prefetch" /> 66 50 67 - <ConsoleCard className="mb-6"> 68 - <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 69 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 70 - </h1> 71 - <Suspense 72 - fallback={ 73 - <> 74 - <div className="min-h-125"> 75 - <QueryTrace 76 - {...getIntentPreloadingQueryTraceProps(currentOffset)} 77 - cacheStatus={getLoadingCacheStatus()} 78 - fetchStatus={getLoadingFetchStatus()} 79 - preloadStatus={getLoadingPreloadStatus()} 80 - /> 81 - <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 82 - </div> 83 - <PaginationNav prevOffset={null} nextOffset={null} to="/intent-preloading" /> 84 - </> 85 - } 86 - > 87 - <PokemonTableContent currentOffset={currentOffset} /> 88 - </Suspense> 89 - </ConsoleCard> 51 + <StrategyPageLayout articleEyebrow="Intent" articleTitle="Hover and focus preloading"> 52 + <ConsoleCard className="mb-6"> 53 + <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 54 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 55 + </h1> 56 + <Suspense 57 + fallback={ 58 + <> 59 + <div className="min-h-125"> 60 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 61 + </div> 62 + <PaginationNav prevOffset={null} nextOffset={null} to="/intent-preloading" /> 63 + </> 64 + } 65 + > 66 + <PokemonTableContent /> 67 + </Suspense> 68 + </ConsoleCard> 69 + </StrategyPageLayout> 90 70 </div> 91 71 </main> 92 72 ); 93 73 } 94 74 95 - function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 75 + function PokemonTableContent() { 96 76 const { pokemonListOptions } = useRouteContext({ from: "/intent-preloading" }); 97 - const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 77 + const { data } = useSuspenseQuery(pokemonListOptions); 98 78 99 79 return ( 100 80 <> 101 81 <div className="min-h-125"> 102 - <QueryTrace 103 - {...getIntentPreloadingQueryTraceProps(currentOffset)} 104 - cacheStatus={getCacheStatus(dataUpdatedAt)} 105 - fetchStatus={getFetchStatus(fetchStatus, status)} 106 - preloadStatus={getPreloadStatus(dataUpdatedAt)} 107 - /> 108 82 <PokemonTable pokemon={data.pokemon} /> 109 83 </div> 110 84 <PaginationNav
+228
src/routes/live-query-filters.tsx
··· 1 + import { Suspense, useCallback, useState } from "react"; 2 + import { createFileRoute, Link } from "@tanstack/react-router"; 3 + import { useLiveSuspenseQuery, eq, ilike, toArray } from "@tanstack/react-db"; 4 + import * as v from "valibot"; 5 + import { StrategyPageLayout } from "~/components/strategy-page-layout"; 6 + import { ConsoleCard } from "~/components/console/console-card"; 7 + import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; 8 + import { SectionHeader } from "~/components/console/section-header"; 9 + import { FilterForm, FilterSubmitContext } from "~/components/filter-form"; 10 + import { pokemonCollection, pokemonTypesCollection, typesCollection } from "~/lib/collections"; 11 + import { cn } from "~/lib/utils"; 12 + import { POKEMON_LIMIT } from "~/constants"; 13 + import { lazily } from "~/util/lazily"; 14 + 15 + const { PokemonTable } = lazily(() => import("~/components/console/pokemon-table")); 16 + 17 + const searchParamsSchema = v.object({ 18 + offset: v.optional(v.number(), 0), 19 + name: v.optional(v.string(), ""), 20 + }); 21 + 22 + function FilterSubmitContextProvider(props: { 23 + initialName: string; 24 + handleSubmit: (nameFilter: string) => void; 25 + children: React.ReactNode; 26 + }) { 27 + const [nameFilter, setNameFilter] = useState(props.initialName); 28 + 29 + const handleSubmit = useCallback(() => { 30 + props.handleSubmit(nameFilter); 31 + }, [nameFilter, props]); 32 + 33 + return ( 34 + <FilterSubmitContext.Provider 35 + value={{ handleSubmit, nameFilter, updateNameFilter: setNameFilter }} 36 + > 37 + {props.children} 38 + </FilterSubmitContext.Provider> 39 + ); 40 + } 41 + 42 + export const Route = createFileRoute("/live-query-filters")({ 43 + validateSearch: searchParamsSchema, 44 + component: RouteComponent, 45 + }); 46 + 47 + function RouteComponent() { 48 + const { offset: currentOffset, name: nameFilter } = Route.useSearch(); 49 + const navigate = Route.useNavigate(); 50 + 51 + return ( 52 + <main className="min-h-screen bg-(--bg-primary) p-6"> 53 + <div className="max-w-7xl mx-auto"> 54 + <SectionHeader title="08_live-query-filters" subtitle="// Electric SQL live search" /> 55 + 56 + <StrategyPageLayout articleEyebrow="Live search" articleTitle="Reactive filtered data"> 57 + <div> 58 + <ConsoleCard className="mb-6"> 59 + <h2 className="text-sm font-semibold mb-4 text-(--text-primary) uppercase tracking-wider"> 60 + Filters 61 + </h2> 62 + <FilterSubmitContextProvider 63 + key={`live-filter-submit-context-provider-${nameFilter}`} 64 + initialName={nameFilter} 65 + handleSubmit={(newNameFilter) => { 66 + void navigate({ 67 + search: { offset: 0, name: newNameFilter }, 68 + }); 69 + }} 70 + > 71 + <FilterForm /> 72 + </FilterSubmitContextProvider> 73 + </ConsoleCard> 74 + 75 + <ConsoleCard> 76 + <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 77 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 78 + {nameFilter && ( 79 + <span className="text-(--text-muted)"> (filtered: &quot;{nameFilter}&quot;)</span> 80 + )} 81 + </h1> 82 + 83 + <Suspense 84 + fallback={ 85 + <> 86 + <div className="min-h-125"> 87 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 88 + </div> 89 + <LiveQueryFiltersPagination 90 + nameFilter={nameFilter} 91 + nextOffset={null} 92 + prevOffset={null} 93 + /> 94 + </> 95 + } 96 + > 97 + <PokemonTableContent currentOffset={currentOffset} nameFilter={nameFilter} /> 98 + </Suspense> 99 + </ConsoleCard> 100 + </div> 101 + </StrategyPageLayout> 102 + </div> 103 + </main> 104 + ); 105 + } 106 + 107 + function PokemonTableContent({ 108 + currentOffset, 109 + nameFilter, 110 + }: { 111 + currentOffset: number; 112 + nameFilter: string; 113 + }) { 114 + const trimmedNameFilter = nameFilter.trim(); 115 + const { data } = useLiveSuspenseQuery( 116 + (q) => { 117 + let query = q.from({ pokemon: pokemonCollection }); 118 + 119 + if (trimmedNameFilter) { 120 + query = query.where(({ pokemon }) => ilike(pokemon.name, `%${trimmedNameFilter}%`)); 121 + } 122 + 123 + return query 124 + .orderBy(({ pokemon }) => pokemon.dexId) 125 + .offset(currentOffset) 126 + .limit(POKEMON_LIMIT + 1) 127 + .select(({ pokemon }) => ({ 128 + id: pokemon.id, 129 + name: pokemon.name, 130 + types: toArray( 131 + q 132 + .from({ pokemonType: pokemonTypesCollection }) 133 + .join( 134 + { type: typesCollection }, 135 + ({ pokemonType, type }) => eq(pokemonType.typeId, type.id), 136 + "inner", 137 + ) 138 + .where(({ pokemonType }) => eq(pokemonType.pokemonId, pokemon.id)) 139 + .orderBy(({ pokemonType }) => pokemonType.id) 140 + .select(({ type }) => ({ name: type.name })), 141 + ), 142 + })); 143 + }, 144 + [currentOffset, trimmedNameFilter], 145 + ); 146 + 147 + const hasMore = data.length > POKEMON_LIMIT; 148 + const pokemon = (hasMore ? data.slice(0, POKEMON_LIMIT) : data).map((pokemon) => ({ 149 + id: pokemon.id, 150 + name: pokemon.name, 151 + types: pokemon.types.map((type) => ({ name: type.name })), 152 + })); 153 + const prevOffset = currentOffset > 0 ? Math.max(0, currentOffset - POKEMON_LIMIT) : null; 154 + const nextOffset = hasMore ? currentOffset + POKEMON_LIMIT : null; 155 + 156 + return ( 157 + <> 158 + <div className="min-h-125"> 159 + <PokemonTable pokemon={pokemon} /> 160 + 161 + {pokemon.length === 0 && nameFilter && ( 162 + <div className="text-center py-4 text-(--text-muted) font-mono text-sm"> 163 + No Pokémon found matching &quot;{nameFilter}&quot; 164 + </div> 165 + )} 166 + </div> 167 + <LiveQueryFiltersPagination 168 + nameFilter={nameFilter} 169 + prevOffset={prevOffset} 170 + nextOffset={nextOffset} 171 + /> 172 + </> 173 + ); 174 + } 175 + 176 + function LiveQueryFiltersPagination(props: { 177 + nameFilter: string; 178 + prevOffset: number | null; 179 + nextOffset: number | null; 180 + }) { 181 + const { nameFilter, prevOffset, nextOffset } = props; 182 + 183 + return ( 184 + <nav className="flex items-center justify-center gap-1 mt-8 font-mono"> 185 + <Link 186 + to="/live-query-filters" 187 + search={{ offset: prevOffset ?? 0, name: nameFilter }} 188 + disabled={prevOffset == null} 189 + className={cn( 190 + "inline-flex items-center gap-2", 191 + "px-3 py-2", 192 + "border border-(--border-default)", 193 + "bg-(--bg-secondary)", 194 + "font-mono text-sm", 195 + "text-(--text-primary)", 196 + "transition-all duration-fast ease-default", 197 + "hover:border-(--accent-default) hover:bg-(--accent-subtle)", 198 + prevOffset == null && "opacity-50 cursor-not-allowed pointer-events-none", 199 + )} 200 + > 201 + <span>&lt;</span> 202 + <span>prev</span> 203 + </Link> 204 + 205 + <span className="px-2 text-(--text-muted)">|</span> 206 + 207 + <Link 208 + to="/live-query-filters" 209 + search={{ offset: nextOffset ?? 0, name: nameFilter }} 210 + disabled={nextOffset == null} 211 + className={cn( 212 + "inline-flex items-center gap-2", 213 + "px-3 py-2", 214 + "border border-(--border-default)", 215 + "bg-(--bg-secondary)", 216 + "font-mono text-sm", 217 + "text-(--text-primary)", 218 + "transition-all duration-fast ease-default", 219 + "hover:border-(--accent-default) hover:bg-(--accent-subtle)", 220 + nextOffset == null && "opacity-50 cursor-not-allowed pointer-events-none", 221 + )} 222 + > 223 + <span>next</span> 224 + <span>&gt;</span> 225 + </Link> 226 + </nav> 227 + ); 228 + }
+55 -30
src/routes/live-query.tsx
··· 1 1 import { Suspense } from "react"; 2 2 import { createFileRoute } from "@tanstack/react-router"; 3 - import { useLiveSuspenseQuery, eq } from "@tanstack/react-db"; 3 + import { useLiveSuspenseQuery, eq, toArray } from "@tanstack/react-db"; 4 4 import * as v from "valibot"; 5 5 import { PaginationNav } from "~/components/pagination-nav"; 6 + import { StrategyPageLayout } from "~/components/strategy-page-layout"; 6 7 import { ConsoleCard } from "~/components/console/console-card"; 7 8 import { SectionHeader } from "~/components/console/section-header"; 8 9 import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; ··· 17 18 }); 18 19 19 20 export const Route = createFileRoute("/live-query")({ 20 - ssr: false, 21 21 validateSearch: searchParamsSchema, 22 22 component: RouteComponent, 23 23 }); ··· 27 27 28 28 return ( 29 29 <main className="min-h-screen bg-(--bg-primary) p-6"> 30 - <div className="max-w-4xl mx-auto"> 31 - <SectionHeader title="04_pagination" subtitle="// Preloading next/prev pages" /> 30 + <div className="max-w-7xl mx-auto"> 31 + <SectionHeader title="07_live-query" subtitle="// Electric SQL synced collection" /> 32 32 33 - <ConsoleCard className="mb-6"> 34 - <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 35 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 36 - </h1> 37 - <Suspense 38 - fallback={ 39 - <> 40 - <div className="min-h-125"> 41 - <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 42 - </div> 43 - <PaginationNav prevOffset={null} nextOffset={null} to="/pagination" /> 44 - </> 45 - } 46 - > 47 - <PokemonTableContent currentOffset={currentOffset} /> 48 - </Suspense> 49 - </ConsoleCard> 33 + <StrategyPageLayout articleEyebrow="Live query" articleTitle="Synced collection"> 34 + <ConsoleCard className="mb-6"> 35 + <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 36 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 37 + </h1> 38 + <Suspense 39 + fallback={ 40 + <> 41 + <div className="min-h-125"> 42 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 43 + </div> 44 + <PaginationNav prevOffset={null} nextOffset={null} to="/live-query" /> 45 + </> 46 + } 47 + > 48 + <PokemonTableContent currentOffset={currentOffset} /> 49 + </Suspense> 50 + </ConsoleCard> 51 + </StrategyPageLayout> 50 52 </div> 51 53 </main> 52 54 ); ··· 54 56 55 57 function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 56 58 const { data } = useLiveSuspenseQuery( 57 - (q) => { 58 - return q 59 - .from({ 60 - pokemon: pokemonCollection, 61 - }) 59 + (q) => 60 + q 61 + .from({ pokemon: pokemonCollection }) 62 62 .orderBy(({ pokemon }) => pokemon.dexId) 63 63 .offset(currentOffset) 64 - .limit(POKEMON_LIMIT); 65 - }, 64 + .limit(POKEMON_LIMIT + 1) 65 + .select(({ pokemon }) => ({ 66 + id: pokemon.id, 67 + name: pokemon.name, 68 + dexId: pokemon.dexId, 69 + types: toArray( 70 + q 71 + .from({ pokemonType: pokemonTypesCollection }) 72 + .join( 73 + { type: typesCollection }, 74 + ({ pokemonType, type }) => eq(pokemonType.typeId, type.id), 75 + "inner", 76 + ) 77 + .where(({ pokemonType }) => eq(pokemonType.pokemonId, pokemon.id)) 78 + .orderBy(({ pokemonType }) => pokemonType.id) 79 + .select(({ type }) => ({ name: type.name })), 80 + ), 81 + })), 66 82 [currentOffset], 67 83 ); 68 - console.log(data); 84 + 85 + const hasMore = data.length > POKEMON_LIMIT; 86 + const pokemon = (hasMore ? data.slice(0, POKEMON_LIMIT) : data).map((pokemon) => ({ 87 + id: pokemon.id, 88 + name: pokemon.name, 89 + types: pokemon.types.map((type) => ({ name: type.name })), 90 + })); 91 + const prevOffset = currentOffset > 0 ? Math.max(0, currentOffset - POKEMON_LIMIT) : null; 92 + const nextOffset = hasMore ? currentOffset + POKEMON_LIMIT : null; 69 93 70 94 return ( 71 95 <> 72 96 <div className="min-h-125"> 73 - <PokemonTable pokemon={data as any} /> 97 + <PokemonTable pokemon={pokemon} /> 74 98 </div> 99 + <PaginationNav prevOffset={prevOffset} nextOffset={nextOffset} to="/live-query" /> 75 100 </> 76 101 ); 77 102 }
+23 -57
src/routes/pagination.tsx
··· 2 2 import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 3 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 - import { QueryTrace } from "~/components/console/query-trace"; 6 - import { 7 - formatPokemonListQueryKey, 8 - getCacheStatus, 9 - getFetchStatus, 10 - getLoadingCacheStatus, 11 - getLoadingFetchStatus, 12 - getLoadingPreloadStatus, 13 - getPreloadStatus, 14 - } from "~/components/console/query-trace-utils"; 15 5 import { PaginationNav } from "~/components/pagination-nav"; 6 + import { StrategyPageLayout } from "~/components/strategy-page-layout"; 16 7 import { ConsoleCard } from "~/components/console/console-card"; 17 8 import { SectionHeader } from "~/components/console/section-header"; 18 9 import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; ··· 26 17 offset: v.optional(v.number(), 0), 27 18 }); 28 19 29 - const getPaginationQueryKeys = (currentOffset: number) => [ 30 - `current: ${formatPokemonListQueryKey("pagination", currentOffset)}`, 31 - ...(currentOffset > 0 32 - ? [`previous: ${formatPokemonListQueryKey("pagination", currentOffset - POKEMON_LIMIT)}`] 33 - : []), 34 - `next: ${formatPokemonListQueryKey("pagination", currentOffset + POKEMON_LIMIT)}`, 35 - ]; 36 - 37 - const getPaginationQueryTraceProps = (currentOffset: number) => ({ 38 - behaviorDescription: 39 - "Viewport preload: the current route is prefetched by the loader, and visible pagination links preload adjacent pages before click.", 40 - queryKeys: getPaginationQueryKeys(currentOffset), 41 - strategyDescription: 'loader prefetchQuery + pagination preload="viewport"', 42 - }); 43 - 44 20 export const Route = createFileRoute("/pagination")({ 45 21 validateSearch: searchParamsSchema, 46 22 loaderDeps: ({ search }) => ({ ··· 69 45 70 46 return ( 71 47 <main className="min-h-screen bg-(--bg-primary) p-6"> 72 - <div className="max-w-4xl mx-auto"> 48 + <div className="max-w-7xl mx-auto"> 73 49 <SectionHeader title="04_pagination" subtitle="// Preloading next/prev pages" /> 74 50 75 - <ConsoleCard className="mb-6"> 76 - <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 77 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 78 - </h1> 79 - <Suspense 80 - fallback={ 81 - <> 82 - <div className="min-h-125"> 83 - <QueryTrace 84 - {...getPaginationQueryTraceProps(currentOffset)} 85 - cacheStatus={getLoadingCacheStatus()} 86 - fetchStatus={getLoadingFetchStatus()} 87 - preloadStatus={getLoadingPreloadStatus()} 88 - /> 89 - <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 90 - </div> 91 - <PaginationNav prevOffset={null} nextOffset={null} to="/pagination" /> 92 - </> 93 - } 94 - > 95 - <PokemonTableContent currentOffset={currentOffset} /> 96 - </Suspense> 97 - </ConsoleCard> 51 + <StrategyPageLayout articleEyebrow="Pagination" articleTitle="Viewport pagination preload"> 52 + <ConsoleCard className="mb-6"> 53 + <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 54 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 55 + </h1> 56 + <Suspense 57 + fallback={ 58 + <> 59 + <div className="min-h-125"> 60 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 61 + </div> 62 + <PaginationNav prevOffset={null} nextOffset={null} to="/pagination" /> 63 + </> 64 + } 65 + > 66 + <PokemonTableContent /> 67 + </Suspense> 68 + </ConsoleCard> 69 + </StrategyPageLayout> 98 70 </div> 99 71 </main> 100 72 ); 101 73 } 102 74 103 - function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 75 + function PokemonTableContent() { 104 76 const { pokemonListOptions } = useRouteContext({ from: "/pagination" }); 105 - const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 77 + const { data } = useSuspenseQuery(pokemonListOptions); 106 78 107 79 return ( 108 80 <> 109 81 <div className="min-h-125"> 110 - <QueryTrace 111 - {...getPaginationQueryTraceProps(currentOffset)} 112 - cacheStatus={getCacheStatus(dataUpdatedAt)} 113 - fetchStatus={getFetchStatus(fetchStatus, status)} 114 - preloadStatus={getPreloadStatus(dataUpdatedAt)} 115 - /> 116 82 <PokemonTable pokemon={data.pokemon} /> 117 83 </div> 118 84 <PaginationNav
+23 -49
src/routes/preloading.tsx
··· 2 2 import { createFileRoute, useRouteContext } from "@tanstack/react-router"; 3 3 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 4 4 import * as v from "valibot"; 5 - import { QueryTrace } from "~/components/console/query-trace"; 6 - import { 7 - formatPokemonListQueryKey, 8 - getCacheStatus, 9 - getFetchStatus, 10 - getLoadingCacheStatus, 11 - getLoadingFetchStatus, 12 - getLoadingPreloadStatus, 13 - getPreloadStatus, 14 - } from "~/components/console/query-trace-utils"; 15 5 import { PaginationNav } from "~/components/pagination-nav"; 6 + import { StrategyPageLayout } from "~/components/strategy-page-layout"; 16 7 import { ConsoleCard } from "~/components/console/console-card"; 17 8 import { SectionHeader } from "~/components/console/section-header"; 18 9 import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; ··· 24 15 25 16 const searchParamsSchema = v.object({ 26 17 offset: v.optional(v.number(), 0), 27 - }); 28 - 29 - const getPreloadingQueryTraceProps = (currentOffset: number) => ({ 30 - behaviorDescription: 31 - "Route-level preload: the loader starts the Pokémon query before this route renders, so the table can use cached data on arrival.", 32 - queryKeys: [formatPokemonListQueryKey("preloading", currentOffset)], 33 - strategyDescription: "loader prefetchQuery", 34 18 }); 35 19 36 20 export const Route = createFileRoute("/preloading")({ ··· 61 45 62 46 return ( 63 47 <main className="min-h-screen bg-(--bg-primary) p-6"> 64 - <div className="max-w-4xl mx-auto"> 48 + <div className="max-w-7xl mx-auto"> 65 49 <SectionHeader title="02_preloading" subtitle="// Route-level prefetch" /> 66 50 67 - <ConsoleCard className="mb-6"> 68 - <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 69 - National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 70 - </h1> 71 - <Suspense 72 - fallback={ 73 - <> 74 - <div className="min-h-125"> 75 - <QueryTrace 76 - {...getPreloadingQueryTraceProps(currentOffset)} 77 - cacheStatus={getLoadingCacheStatus()} 78 - fetchStatus={getLoadingFetchStatus()} 79 - preloadStatus={getLoadingPreloadStatus()} 80 - /> 81 - <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 82 - </div> 83 - <PaginationNav prevOffset={null} nextOffset={null} to="/preloading" /> 84 - </> 85 - } 86 - > 87 - <PokemonTableContent currentOffset={currentOffset} /> 88 - </Suspense> 89 - </ConsoleCard> 51 + <StrategyPageLayout articleEyebrow="Route loader" articleTitle="Route-level prefetch"> 52 + <ConsoleCard className="mb-6"> 53 + <h1 className="text-lg font-mono text-(--text-primary) mb-4"> 54 + National Pokédex: Pokémon {currentOffset + 1}-{currentOffset + POKEMON_LIMIT} 55 + </h1> 56 + <Suspense 57 + fallback={ 58 + <> 59 + <div className="min-h-125"> 60 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 61 + </div> 62 + <PaginationNav prevOffset={null} nextOffset={null} to="/preloading" /> 63 + </> 64 + } 65 + > 66 + <PokemonTableContent /> 67 + </Suspense> 68 + </ConsoleCard> 69 + </StrategyPageLayout> 90 70 </div> 91 71 </main> 92 72 ); 93 73 } 94 74 95 - function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 75 + function PokemonTableContent() { 96 76 const { pokemonListOptions } = useRouteContext({ from: "/preloading" }); 97 - const { data, dataUpdatedAt, fetchStatus, status } = useSuspenseQuery(pokemonListOptions); 77 + const { data } = useSuspenseQuery(pokemonListOptions); 98 78 99 79 return ( 100 80 <> 101 81 <div className="min-h-125"> 102 - <QueryTrace 103 - {...getPreloadingQueryTraceProps(currentOffset)} 104 - cacheStatus={getCacheStatus(dataUpdatedAt)} 105 - fetchStatus={getFetchStatus(fetchStatus, status)} 106 - preloadStatus={getPreloadStatus(dataUpdatedAt)} 107 - /> 108 82 <PokemonTable pokemon={data.pokemon} /> 109 83 </div> 110 84 <PaginationNav prevOffset={data.prevOffset} nextOffset={data.nextOffset} to="/preloading" />