this repo has no description
0
fork

Configure Feed

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

a bunch of changes

+243 -123
+1 -1
CONTEXT.md
··· 61 61 - The **Chapters** vertical owns chapter framing and status language, including section headers and status indicators. 62 62 - The **Landing** vertical owns home-page presentation and consumes the **Chapters** vertical for the table of contents. 63 63 - The **Design System** vertical owns primitive UI components, CSS files, styling tokens, utility class merging, and the theme toggle. 64 - - The **Demo** vertical owns console cards and panel frames used by the interactive demo surface. 64 + - The **Demo** vertical owns demo cards and panel frames used by the interactive demo surface. 65 65 - Small implementation helpers used only by one vertical belong to that vertical; for example, the lazy component helper belongs to **Demo** while only the demo table uses it. 66 66 - **Framework adapters** may remain in TanStack Start, TanStack Router, or Vite+ default locations even when they register behavior for a vertical. 67 67 - API shape route files are **Framework adapters**; **Demo** owns the Electric collections that depend on those route URLs.
+4 -4
DESIGN.md
··· 56 56 - `--text-xs`: dense labels, table headings, badges. 57 57 - `--text-sm`: default body and controls. 58 58 - `--text-base`: landing descriptions and prominent card titles. 59 - - `--text-lg`: console card headings. 59 + - `--text-lg`: demo card headings. 60 60 - `--text-xl` and above: section headings and hero headings. 61 61 62 62 Uppercase labels should be short. Avoid long uppercase prose because it harms scan speed. ··· 68 68 - Sticky horizontal navigation at the top. 69 69 - Landing page with a concise intro and grouped example links. 70 70 - Example pages with `max-w-7xl`, 24px page padding, a section header, then a strategy/content split. 71 - - Console cards for data tables, filters, skeletons, and controls. 71 + - Demo cards for data tables, filters, skeletons, and controls. 72 72 73 73 The strategy route layout uses: 74 74 75 75 - Left column: explanation panel through `StrategyArticle`. 76 - - Right column: interactive console content through `ConsoleCard`. 76 + - Right column: interactive demo content through `DemoCard`. 77 77 - Desktop split: `minmax(0,0.9fr)` and `minmax(34rem,1.1fr)`. 78 78 79 79 Keep table areas dimensionally stable while loading. Existing examples reserve height with `min-h-125` and skeleton rows; preserve that pattern to prevent layout jump. ··· 94 94 - The recommended starting example uses `--accent-surface` and an uppercase badge. 95 95 - Keep card copy short and behavior-specific. 96 96 97 - **ConsoleCard** 97 + **DemoCard** 98 98 99 99 - Primary framed work surface. 100 100 - White or dark card background via `--bg-card`.
+1
package.json
··· 35 35 "postgres": "^3.4.9", 36 36 "react": "^19.2.5", 37 37 "react-dom": "^19.2.5", 38 + "react-error-boundary": "^6.1.1", 38 39 "rehype-raw": "^7.0.0", 39 40 "rehype-react": "^8.0.0", 40 41 "remark-gfm": "^4.0.1",
+12
pnpm-lock.yaml
··· 78 78 react-dom: 79 79 specifier: ^19.2.5 80 80 version: 19.2.5(react@19.2.5) 81 + react-error-boundary: 82 + specifier: ^6.1.1 83 + version: 6.1.1(react@19.2.5) 81 84 rehype-raw: 82 85 specifier: ^7.0.0 83 86 version: 7.0.0 ··· 4704 4707 resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} 4705 4708 peerDependencies: 4706 4709 react: ^19.2.5 4710 + 4711 + react-error-boundary@6.1.1: 4712 + resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} 4713 + peerDependencies: 4714 + react: ^18.0.0 || ^19.0.0 4707 4715 4708 4716 react@19.2.5: 4709 4717 resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} ··· 10165 10173 dependencies: 10166 10174 react: 19.2.5 10167 10175 scheduler: 0.27.0 10176 + 10177 + react-error-boundary@6.1.1(react@19.2.5): 10178 + dependencies: 10179 + react: 19.2.5 10168 10180 10169 10181 react@19.2.5: {} 10170 10182
+12 -3
src/articles/article.functions.tsx
··· 1 1 import { createServerFn } from "@tanstack/react-start"; 2 + import { queryOptions } from "@tanstack/react-query"; 2 3 import { renderServerComponent } from "@tanstack/react-start/rsc"; 3 4 import * as v from "valibot"; 5 + 4 6 import { ArticleShell } from "~/articles/article-shell.server"; 5 7 6 8 const articlesContent: Record<string, () => Promise<string>> = { ··· 15 17 "live-query-filters": () => import("./content/live-query-filters.md?raw").then((m) => m.default), 16 18 }; 17 19 18 - export const getArticle = createServerFn({ method: "GET" }) 20 + const getServerArticle = createServerFn({ method: "GET" }) 19 21 .inputValidator(v.object({ title: v.string(), slug: v.string() })) 20 22 .handler(async ({ data }) => { 21 23 const markdown = await (articlesContent[data.slug]?.() ?? 22 24 Promise.resolve("Content coming soon.")); 23 25 24 - const Renderable = await renderServerComponent( 26 + const article = await renderServerComponent( 25 27 <ArticleShell title={data.title} markdown={markdown} />, 26 28 ); 27 29 28 - return { Renderable }; 30 + return { article }; 31 + }); 32 + 33 + export const getArticleQueryOptions = ({ title, slug }: { title: string; slug: string }) => 34 + queryOptions({ 35 + queryKey: ["article", { title, slug }], 36 + structuralSharing: false, 37 + queryFn: () => getServerArticle({ data: { title, slug } }), 29 38 });
+5 -5
src/demos/components/console-card.tsx src/demos/components/demo-card.tsx
··· 1 1 import { cn } from "~/design-system/utils/cn"; 2 2 import type { ReactNode } from "react"; 3 3 4 - interface ConsoleCardProps { 4 + interface DemoCardProps { 5 5 children: ReactNode; 6 6 className?: string; 7 7 hover?: boolean; ··· 10 10 /** 11 11 * Primary content container with hairline border. 12 12 * 13 - * The console aesthetic uses zero border-radius and visible borders 13 + * The demo aesthetic uses zero border-radius and visible borders 14 14 * to create a technical, information-dense feel. 15 15 * 16 16 * @example 17 - * <ConsoleCard> 17 + * <DemoCard> 18 18 * <h2>Content</h2> 19 - * </ConsoleCard> 19 + * </DemoCard> 20 20 */ 21 - export function ConsoleCard({ children, className, hover = false }: ConsoleCardProps) { 21 + export function DemoCard({ children, className, hover = false }: DemoCardProps) { 22 22 return ( 23 23 <div 24 24 className={cn(
+25
src/demos/components/demo-error-fallback.tsx
··· 1 + import type { FallbackProps } from "react-error-boundary"; 2 + import { Button } from "~/design-system/ui/button"; 3 + 4 + export function DemoErrorFallback({ error, resetErrorBoundary }: FallbackProps) { 5 + return ( 6 + <div className="min-h-125 flex flex-col items-center justify-center gap-4 border border-(--border-default) bg-(--bg-card) p-6"> 7 + <div className="text-center space-y-2"> 8 + <p className="text-sm font-mono font-semibold text-(--status-error)"> 9 + Failed to load Pok&eacute;mon data 10 + </p> 11 + <p className="max-w-md text-xs font-mono text-(--text-muted) leading-relaxed"> 12 + {getErrorMessage(error)} 13 + </p> 14 + </div> 15 + <Button onClick={resetErrorBoundary} size="sm" className="font-mono"> 16 + Try again 17 + </Button> 18 + </div> 19 + ); 20 + } 21 + 22 + function getErrorMessage(error: unknown): string { 23 + if (error instanceof Error) return error.message; 24 + return "An unknown error occurred"; 25 + }
+18 -12
src/demos/components/pokedex-table-section.tsx
··· 1 1 import * as React from "react"; 2 + import { ErrorBoundary } from "react-error-boundary"; 3 + import { useQueryErrorResetBoundary } from "@tanstack/react-query"; 2 4 import { POKEMON_LIMIT } from "~/demos/pokemon-listing/constants"; 3 5 import { lazily } from "~/vendor/lazily"; 4 6 import { PaginationNav } from "~/demos/components/pagination-nav"; 7 + import { DemoErrorFallback } from "./demo-error-fallback"; 5 8 import { PokemonTableSkeleton } from "./pokemon-table-skeleton"; 6 9 7 10 const { PokemonTable } = lazily(() => import("./pokemon-table")); ··· 21 24 22 25 export function PokedexTableSection(props: PokedexTableSectionProps) { 23 26 const { children, currentOffset, fallbackPagination, nameFilter } = props; 27 + const { reset } = useQueryErrorResetBoundary(); 24 28 25 29 return ( 26 30 <> ··· 30 34 <span className="text-(--text-muted)"> (filtered: &quot;{nameFilter}&quot;)</span> 31 35 )} 32 36 </h1> 33 - <React.Suspense 34 - fallback={ 35 - <> 36 - <PokedexTableShell> 37 - <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 38 - </PokedexTableShell> 39 - {fallbackPagination} 40 - </> 41 - } 42 - > 43 - {children} 44 - </React.Suspense> 37 + <ErrorBoundary onReset={reset} FallbackComponent={DemoErrorFallback}> 38 + <React.Suspense 39 + fallback={ 40 + <> 41 + <PokedexTableShell> 42 + <PokemonTableSkeleton rowCount={POKEMON_LIMIT} /> 43 + </PokedexTableShell> 44 + {fallbackPagination} 45 + </> 46 + } 47 + > 48 + {children} 49 + </React.Suspense> 50 + </ErrorBoundary> 45 51 </> 46 52 ); 47 53 }
+19 -11
src/routes/_chapters/basic.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 2 import { useSuspenseQuery } from "@tanstack/react-query"; 3 3 import * as v from "valibot"; 4 - import { ConsoleCard } from "~/demos/components/console-card"; 4 + import { DemoCard } from "~/demos/components/demo-card"; 5 5 import { 6 6 PokedexPagination, 7 7 PokedexTableResults, 8 8 PokedexTableSection, 9 9 } from "~/demos/components/pokedex-table-section"; 10 10 import { getPokemonListQueryFn, getPokemonListQueryKey } from "~/demos/query/pokemon-query"; 11 - import { getArticle } from "~/articles/article.functions"; 11 + import { getArticleQueryOptions } from "~/articles/article.functions"; 12 12 import { ChapterSplitColumn } from "~/chapters/chapter-split"; 13 13 14 14 const searchParamsSchema = v.object({ ··· 20 20 routeTitle: "01_basic", 21 21 routeSubtitle: "// Baseline fetching", 22 22 }, 23 - validateSearch: searchParamsSchema, 24 - loader: async () => { 25 - const { Renderable: Article } = await getArticle({ 26 - data: { title: "No prefetching", slug: "basic" }, 23 + context: () => { 24 + const noPrefetchArticleQueryOptions = getArticleQueryOptions({ 25 + title: "No prefetching", 26 + slug: "basic", 27 27 }); 28 - return { Article }; 28 + return { noPrefetchArticleQueryOptions }; 29 + }, 30 + validateSearch: searchParamsSchema, 31 + loader: async ({ context: { queryClient, noPrefetchArticleQueryOptions } }) => { 32 + await queryClient.ensureQueryData(noPrefetchArticleQueryOptions); 29 33 }, 30 34 component: RouteComponent, 31 35 }); 32 36 33 37 function RouteComponent() { 34 38 const { offset: currentOffset } = Route.useSearch(); 35 - const { Article } = Route.useLoaderData(); 39 + const { noPrefetchArticleQueryOptions } = Route.useRouteContext(); 40 + 41 + const { 42 + data: { article }, 43 + } = useSuspenseQuery(noPrefetchArticleQueryOptions); 36 44 37 45 return ( 38 46 <ChapterSplitColumn 39 - blog={Article} 47 + blog={article} 40 48 table={ 41 - <ConsoleCard className="mb-6"> 49 + <DemoCard className="mb-6"> 42 50 <PokedexTableSection 43 51 currentOffset={currentOffset} 44 52 fallbackPagination={ ··· 47 55 > 48 56 <PokemonTableContent currentOffset={currentOffset} /> 49 57 </PokedexTableSection> 50 - </ConsoleCard> 58 + </DemoCard> 51 59 } 52 60 /> 53 61 );
+24 -15
src/routes/_chapters/debounced-preload-filters.tsx
··· 4 4 import * as v from "valibot"; 5 5 import { FilterForm } from "~/demos/components/filter-form"; 6 6 import { ChapterSplitColumn } from "~/chapters/chapter-split"; 7 - import { ConsoleCard } from "~/demos/components/console-card"; 7 + import { DemoCard } from "~/demos/components/demo-card"; 8 8 import { 9 9 PokedexPagination, 10 10 PokedexTableResults, ··· 14 14 getFilteredPokemonListQueryKey, 15 15 getFilteredPokemonListQueryFn, 16 16 } from "~/demos/query/pokemon-query"; 17 - import { getArticle } from "~/articles/article.functions"; 17 + import { getArticleQueryOptions } from "~/articles/article.functions"; 18 18 19 19 const searchParamsSchema = v.object({ 20 20 offset: v.optional(v.number(), 0), ··· 43 43 queryFn: getFilteredPokemonListQueryFn, 44 44 }); 45 45 46 + const debouncedArticleQueryOptions = getArticleQueryOptions({ 47 + title: "Debounced filter prefetch", 48 + slug: "debounced-preload-filters", 49 + }); 50 + 46 51 return { 47 52 pokemonListOptions, 53 + debouncedArticleQueryOptions, 48 54 }; 49 55 }, 50 - loader: async ({ context }) => { 51 - void context.queryClient.prefetchQuery(context.pokemonListOptions); 52 - const { Renderable: Article } = await getArticle({ 53 - data: { title: "Debounced filter prefetch", slug: "debounced-preload-filters" }, 54 - }); 55 - return { Article }; 56 + loader: async ({ 57 + context: { queryClient, pokemonListOptions, debouncedArticleQueryOptions }, 58 + }) => { 59 + void queryClient.prefetchQuery(pokemonListOptions); 60 + await queryClient.ensureQueryData(debouncedArticleQueryOptions); 56 61 }, 57 62 component: RouteComponent, 58 63 }); 59 64 60 65 function RouteComponent() { 61 66 const { offset: currentOffset, name: nameFilter } = Route.useSearch(); 62 - const { Article } = Route.useLoaderData(); 67 + const { debouncedArticleQueryOptions, pokemonListOptions: serverPokemonListOptions } = 68 + Route.useRouteContext(); 63 69 const queryClient = useQueryClient(); 64 - const { pokemonListOptions: serverPokemonListOptions } = Route.useRouteContext(); 70 + 71 + const { 72 + data: { article }, 73 + } = useSuspenseQuery(debouncedArticleQueryOptions); 65 74 const navigate = Route.useNavigate(); 66 75 const debouncedNameFilter = useDebouncedCallback( 67 76 (newNameFilter: string) => { ··· 81 90 82 91 return ( 83 92 <ChapterSplitColumn 84 - blog={Article} 93 + blog={article} 85 94 table={ 86 95 <> 87 - <ConsoleCard className="mb-6"> 96 + <DemoCard className="mb-6"> 88 97 <FilterForm 89 98 initialName={nameFilter} 90 99 description="Preloads results while typing (debounced 100ms)" ··· 95 104 }); 96 105 }} 97 106 /> 98 - </ConsoleCard> 107 + </DemoCard> 99 108 100 - <ConsoleCard> 109 + <DemoCard> 101 110 <PokedexTableSection 102 111 currentOffset={currentOffset} 103 112 nameFilter={nameFilter} ··· 111 120 > 112 121 <PokemonTableContent nameFilter={nameFilter} /> 113 122 </PokedexTableSection> 114 - </ConsoleCard> 123 + </DemoCard> 115 124 </> 116 125 } 117 126 />
+21 -14
src/routes/_chapters/filters.tsx
··· 3 3 import * as v from "valibot"; 4 4 import { FilterForm } from "~/demos/components/filter-form"; 5 5 import { ChapterSplitColumn } from "~/chapters/chapter-split"; 6 - import { ConsoleCard } from "~/demos/components/console-card"; 6 + import { DemoCard } from "~/demos/components/demo-card"; 7 7 import { 8 8 PokedexPagination, 9 9 PokedexTableResults, ··· 13 13 getFilteredPokemonListQueryKey, 14 14 getFilteredPokemonListQueryFn, 15 15 } from "~/demos/query/pokemon-query"; 16 - import { getArticle } from "~/articles/article.functions"; 16 + import { getArticleQueryOptions } from "~/articles/article.functions"; 17 17 18 18 const searchParamsSchema = v.object({ 19 19 offset: v.optional(v.number(), 0), ··· 38 38 queryFn: getFilteredPokemonListQueryFn, 39 39 }); 40 40 41 + const filtersArticleQueryOptions = getArticleQueryOptions({ 42 + title: "Submitted filter prefetch", 43 + slug: "filters", 44 + }); 45 + 41 46 return { 42 47 pokemonListOptions, 48 + filtersArticleQueryOptions, 43 49 }; 44 50 }, 45 - loader: async ({ context }) => { 46 - void context.queryClient.prefetchQuery(context.pokemonListOptions); 47 - const { Renderable: Article } = await getArticle({ 48 - data: { title: "Submitted filter prefetch", slug: "filters" }, 49 - }); 50 - return { Article }; 51 + loader: async ({ context: { queryClient, pokemonListOptions, filtersArticleQueryOptions } }) => { 52 + void queryClient.prefetchQuery(pokemonListOptions); 53 + await queryClient.ensureQueryData(filtersArticleQueryOptions); 51 54 }, 52 55 component: RouteComponent, 53 56 }); 54 57 55 58 function RouteComponent() { 56 59 const { offset: currentOffset, name: nameFilter } = Route.useSearch(); 57 - const { Article } = Route.useLoaderData(); 60 + const { filtersArticleQueryOptions } = Route.useRouteContext(); 58 61 const navigate = Route.useNavigate(); 59 62 63 + const { 64 + data: { article }, 65 + } = useSuspenseQuery(filtersArticleQueryOptions); 66 + 60 67 return ( 61 68 <ChapterSplitColumn 62 - blog={Article} 69 + blog={article} 63 70 table={ 64 71 <> 65 - <ConsoleCard className="mb-6"> 72 + <DemoCard className="mb-6"> 66 73 <FilterForm 67 74 initialName={nameFilter} 68 75 onSubmit={(newNameFilter) => { ··· 71 78 }); 72 79 }} 73 80 /> 74 - </ConsoleCard> 81 + </DemoCard> 75 82 76 - <ConsoleCard> 83 + <DemoCard> 77 84 <PokedexTableSection 78 85 currentOffset={currentOffset} 79 86 nameFilter={nameFilter} ··· 83 90 > 84 91 <PokemonTableContent nameFilter={nameFilter} /> 85 92 </PokedexTableSection> 86 - </ConsoleCard> 93 + </DemoCard> 87 94 </> 88 95 } 89 96 />
+19 -12
src/routes/_chapters/intent-preloading.tsx
··· 2 2 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 3 3 import * as v from "valibot"; 4 4 import { ChapterSplitColumn } from "~/chapters/chapter-split"; 5 - import { ConsoleCard } from "~/demos/components/console-card"; 5 + import { DemoCard } from "~/demos/components/demo-card"; 6 6 import { 7 7 PokedexPagination, 8 8 PokedexTableResults, 9 9 PokedexTableSection, 10 10 } from "~/demos/components/pokedex-table-section"; 11 11 import { getPokemonListQueryKey, getPokemonListQueryFn } from "~/demos/query/pokemon-query"; 12 - import { getArticle } from "~/articles/article.functions"; 12 + import { getArticleQueryOptions } from "~/articles/article.functions"; 13 13 14 14 const searchParamsSchema = v.object({ 15 15 offset: v.optional(v.number(), 0), ··· 32 32 queryFn: getPokemonListQueryFn, 33 33 }); 34 34 35 + const intentArticleQueryOptions = getArticleQueryOptions({ 36 + title: "Hover and focus preloading", 37 + slug: "intent-preloading", 38 + }); 39 + 35 40 return { 36 41 pokemonListOptions, 42 + intentArticleQueryOptions, 37 43 }; 38 44 }, 39 - loader: async ({ context }) => { 40 - void context.queryClient.prefetchQuery(context.pokemonListOptions); 41 - const { Renderable: Article } = await getArticle({ 42 - data: { title: "Hover and focus preloading", slug: "intent-preloading" }, 43 - }); 44 - return { Article }; 45 + loader: async ({ context: { queryClient, pokemonListOptions, intentArticleQueryOptions } }) => { 46 + void queryClient.prefetchQuery(pokemonListOptions); 47 + await queryClient.ensureQueryData(intentArticleQueryOptions); 45 48 }, 46 49 component: RouteComponent, 47 50 }); 48 51 49 52 function RouteComponent() { 50 53 const { offset: currentOffset } = Route.useSearch(); 51 - const { Article } = Route.useLoaderData(); 54 + const { intentArticleQueryOptions } = Route.useRouteContext(); 55 + 56 + const { 57 + data: { article }, 58 + } = useSuspenseQuery(intentArticleQueryOptions); 52 59 53 60 return ( 54 61 <ChapterSplitColumn 55 - blog={Article} 62 + blog={article} 56 63 table={ 57 - <ConsoleCard className="mb-6"> 64 + <DemoCard className="mb-6"> 58 65 <PokedexTableSection 59 66 currentOffset={currentOffset} 60 67 fallbackPagination={ ··· 63 70 > 64 71 <PokemonTableContent /> 65 72 </PokedexTableSection> 66 - </ConsoleCard> 73 + </DemoCard> 67 74 } 68 75 /> 69 76 );
+21 -12
src/routes/_chapters/live-query-filters.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useSuspenseQuery } from "@tanstack/react-query"; 2 3 import { useLiveSuspenseQuery, eq, ilike, toArray } from "@tanstack/react-db"; 3 4 import * as v from "valibot"; 4 5 import { ChapterSplitColumn } from "~/chapters/chapter-split"; 5 - import { ConsoleCard } from "~/demos/components/console-card"; 6 + import { DemoCard } from "~/demos/components/demo-card"; 6 7 import { FilterForm } from "~/demos/components/filter-form"; 7 8 import { 8 9 PokedexPagination, ··· 19 20 normalizePokemonNameFilter, 20 21 toPokemonListing, 21 22 } from "~/demos/pokemon-listing/pokemon-listing"; 22 - import { getArticle } from "~/articles/article.functions"; 23 + import { getArticleQueryOptions } from "~/articles/article.functions"; 23 24 24 25 const searchParamsSchema = v.object({ 25 26 offset: v.optional(v.number(), 0), ··· 33 34 }, 34 35 ssr: false, 35 36 validateSearch: searchParamsSchema, 36 - loader: async () => { 37 - const { Renderable: Article } = await getArticle({ 38 - data: { title: "Reactive filtered data", slug: "live-query-filters" }, 37 + context: () => { 38 + const liveQueryFiltersArticleQueryOptions = getArticleQueryOptions({ 39 + title: "Reactive filtered data", 40 + slug: "live-query-filters", 39 41 }); 40 - return { Article }; 42 + return { liveQueryFiltersArticleQueryOptions }; 43 + }, 44 + loader: async ({ context: { queryClient, liveQueryFiltersArticleQueryOptions } }) => { 45 + await queryClient.ensureQueryData(liveQueryFiltersArticleQueryOptions); 41 46 }, 42 47 component: RouteComponent, 43 48 }); 44 49 45 50 function RouteComponent() { 46 51 const { offset: currentOffset, name: nameFilter } = Route.useSearch(); 47 - const { Article } = Route.useLoaderData(); 52 + const { liveQueryFiltersArticleQueryOptions } = Route.useRouteContext(); 48 53 const navigate = Route.useNavigate(); 49 54 55 + const { 56 + data: { article }, 57 + } = useSuspenseQuery(liveQueryFiltersArticleQueryOptions); 58 + 50 59 return ( 51 60 <ChapterSplitColumn 52 - blog={Article} 61 + blog={article} 53 62 table={ 54 63 <> 55 - <ConsoleCard className="mb-6"> 64 + <DemoCard className="mb-6"> 56 65 <FilterForm 57 66 initialName={nameFilter} 58 67 onSubmit={(newNameFilter) => { ··· 61 70 }); 62 71 }} 63 72 /> 64 - </ConsoleCard> 73 + </DemoCard> 65 74 66 - <ConsoleCard> 75 + <DemoCard> 67 76 <PokedexTableSection 68 77 currentOffset={currentOffset} 69 78 nameFilter={nameFilter} ··· 78 87 > 79 88 <PokemonTableContent currentOffset={currentOffset} nameFilter={nameFilter} /> 80 89 </PokedexTableSection> 81 - </ConsoleCard> 90 + </DemoCard> 82 91 </> 83 92 } 84 93 />
+19 -10
src/routes/_chapters/live-query.tsx
··· 1 1 import { createFileRoute } from "@tanstack/react-router"; 2 + import { useSuspenseQuery } from "@tanstack/react-query"; 2 3 import { useLiveSuspenseQuery, eq, toArray } from "@tanstack/react-db"; 3 4 import * as v from "valibot"; 4 5 import { ChapterSplitColumn } from "~/chapters/chapter-split"; 5 - import { ConsoleCard } from "~/demos/components/console-card"; 6 + import { DemoCard } from "~/demos/components/demo-card"; 6 7 import { 7 8 PokedexPagination, 8 9 PokedexTableResults, 9 10 PokedexTableSection, 10 11 } from "~/demos/components/pokedex-table-section"; 11 - import { getArticle } from "~/articles/article.functions"; 12 + import { getArticleQueryOptions } from "~/articles/article.functions"; 12 13 import { 13 14 pokemonCollection, 14 15 typesCollection, ··· 30 31 }, 31 32 ssr: false, 32 33 validateSearch: searchParamsSchema, 33 - loader: async () => { 34 - const { Renderable: Article } = await getArticle({ 35 - data: { title: "Synced collection", slug: "live-query" }, 34 + context: () => { 35 + const liveQueryArticleQueryOptions = getArticleQueryOptions({ 36 + title: "Synced collection", 37 + slug: "live-query", 36 38 }); 37 - return { Article }; 39 + return { liveQueryArticleQueryOptions }; 40 + }, 41 + loader: async ({ context: { queryClient, liveQueryArticleQueryOptions } }) => { 42 + await queryClient.ensureQueryData(liveQueryArticleQueryOptions); 38 43 }, 39 44 component: RouteComponent, 40 45 }); 41 46 42 47 function RouteComponent() { 43 48 const { offset: currentOffset } = Route.useSearch(); 44 - const { Article } = Route.useLoaderData(); 49 + const { liveQueryArticleQueryOptions } = Route.useRouteContext(); 50 + 51 + const { 52 + data: { article }, 53 + } = useSuspenseQuery(liveQueryArticleQueryOptions); 45 54 46 55 return ( 47 56 <ChapterSplitColumn 48 - blog={Article} 57 + blog={article} 49 58 table={ 50 - <ConsoleCard className="mb-6"> 59 + <DemoCard className="mb-6"> 51 60 <PokedexTableSection 52 61 currentOffset={currentOffset} 53 62 fallbackPagination={ ··· 56 65 > 57 66 <PokemonTableContent currentOffset={currentOffset} /> 58 67 </PokedexTableSection> 59 - </ConsoleCard> 68 + </DemoCard> 60 69 } 61 70 /> 62 71 );
+21 -12
src/routes/_chapters/pagination.tsx
··· 2 2 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 3 3 import * as v from "valibot"; 4 4 import { ChapterSplitColumn } from "~/chapters/chapter-split"; 5 - import { ConsoleCard } from "~/demos/components/console-card"; 5 + import { DemoCard } from "~/demos/components/demo-card"; 6 6 import { 7 7 PokedexPagination, 8 8 PokedexTableResults, 9 9 PokedexTableSection, 10 10 } from "~/demos/components/pokedex-table-section"; 11 11 import { getPokemonListQueryKey, getPokemonListQueryFn } from "~/demos/query/pokemon-query"; 12 - import { getArticle } from "~/articles/article.functions"; 12 + import { getArticleQueryOptions } from "~/articles/article.functions"; 13 13 14 14 const searchParamsSchema = v.object({ 15 15 offset: v.optional(v.number(), 0), ··· 30 30 const pokemonListOptions = queryOptions({ 31 31 queryKey: newKey, 32 32 queryFn: getPokemonListQueryFn, 33 + }); 34 + 35 + const paginationArticleQueryOptions = getArticleQueryOptions({ 36 + title: "Viewport pagination preload", 37 + slug: "pagination", 33 38 }); 34 39 35 40 return { 36 41 pokemonListOptions, 42 + paginationArticleQueryOptions, 37 43 }; 38 44 }, 39 - loader: async ({ context }) => { 40 - void context.queryClient.prefetchQuery(context.pokemonListOptions); 41 - const { Renderable: Article } = await getArticle({ 42 - data: { title: "Viewport pagination preload", slug: "pagination" }, 43 - }); 44 - return { Article }; 45 + loader: async ({ 46 + context: { queryClient, pokemonListOptions, paginationArticleQueryOptions }, 47 + }) => { 48 + void queryClient.prefetchQuery(pokemonListOptions); 49 + await queryClient.ensureQueryData(paginationArticleQueryOptions); 45 50 }, 46 51 component: RouteComponent, 47 52 }); 48 53 49 54 function RouteComponent() { 50 55 const { offset: currentOffset } = Route.useSearch(); 51 - const { Article } = Route.useLoaderData(); 56 + const { paginationArticleQueryOptions } = Route.useRouteContext(); 57 + 58 + const { 59 + data: { article }, 60 + } = useSuspenseQuery(paginationArticleQueryOptions); 52 61 53 62 return ( 54 63 <ChapterSplitColumn 55 - blog={Article} 64 + blog={article} 56 65 table={ 57 - <ConsoleCard className="mb-6"> 66 + <DemoCard className="mb-6"> 58 67 <PokedexTableSection 59 68 currentOffset={currentOffset} 60 69 fallbackPagination={ ··· 63 72 > 64 73 <PokemonTableContent /> 65 74 </PokedexTableSection> 66 - </ConsoleCard> 75 + </DemoCard> 67 76 } 68 77 /> 69 78 );
+21 -12
src/routes/_chapters/preloading.tsx
··· 2 2 import { queryOptions, useSuspenseQuery } from "@tanstack/react-query"; 3 3 import * as v from "valibot"; 4 4 import { ChapterSplitColumn } from "~/chapters/chapter-split"; 5 - import { ConsoleCard } from "~/demos/components/console-card"; 5 + import { DemoCard } from "~/demos/components/demo-card"; 6 6 import { 7 7 PokedexPagination, 8 8 PokedexTableResults, 9 9 PokedexTableSection, 10 10 } from "~/demos/components/pokedex-table-section"; 11 11 import { getPokemonListQueryKey, getPokemonListQueryFn } from "~/demos/query/pokemon-query"; 12 - import { getArticle } from "~/articles/article.functions"; 12 + import { getArticleQueryOptions } from "~/articles/article.functions"; 13 13 14 14 const searchParamsSchema = v.object({ 15 15 offset: v.optional(v.number(), 0), ··· 30 30 const pokemonListOptions = queryOptions({ 31 31 queryKey: newKey, 32 32 queryFn: getPokemonListQueryFn, 33 + }); 34 + 35 + const preloadingArticleQueryOptions = getArticleQueryOptions({ 36 + title: "Route-level prefetch", 37 + slug: "preloading", 33 38 }); 34 39 35 40 return { 36 41 pokemonListOptions, 42 + preloadingArticleQueryOptions, 37 43 }; 38 44 }, 39 - loader: async ({ context }) => { 40 - void context.queryClient.prefetchQuery(context.pokemonListOptions); 41 - const { Renderable: Article } = await getArticle({ 42 - data: { title: "Route-level prefetch", slug: "preloading" }, 43 - }); 44 - return { Article }; 45 + loader: async ({ 46 + context: { queryClient, pokemonListOptions, preloadingArticleQueryOptions }, 47 + }) => { 48 + void queryClient.prefetchQuery(pokemonListOptions); 49 + await queryClient.ensureQueryData(preloadingArticleQueryOptions); 45 50 }, 46 51 component: RouteComponent, 47 52 }); 48 53 49 54 function RouteComponent() { 50 55 const { offset: currentOffset } = Route.useSearch(); 51 - const { Article } = Route.useLoaderData(); 56 + const { preloadingArticleQueryOptions } = Route.useRouteContext(); 57 + 58 + const { 59 + data: { article }, 60 + } = useSuspenseQuery(preloadingArticleQueryOptions); 52 61 53 62 return ( 54 63 <ChapterSplitColumn 55 - blog={Article} 64 + blog={article} 56 65 table={ 57 - <ConsoleCard className="mb-6"> 66 + <DemoCard className="mb-6"> 58 67 <PokedexTableSection 59 68 currentOffset={currentOffset} 60 69 fallbackPagination={ ··· 63 72 > 64 73 <PokemonTableContent /> 65 74 </PokedexTableSection> 66 - </ConsoleCard> 75 + </DemoCard> 67 76 } 68 77 /> 69 78 );