this repo has no description
0
fork

Configure Feed

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

add a broke ass live query route

+330
+7
src/data/schema.ts
··· 1 1 import { pgTable, index, integer, text } from "drizzle-orm/pg-core"; 2 + import { createSelectSchema } from "drizzle-orm/valibot"; 2 3 3 4 export const pokemon = pgTable("pokemon", { 4 5 id: integer().primaryKey(), 5 6 name: text().notNull(), 6 7 dexId: integer("dex_id").notNull(), 7 8 }); 9 + 10 + export const pokemonSelectSchema = createSelectSchema(pokemon); 8 11 9 12 export const pokemonTypes = pgTable( 10 13 "pokemon_types", ··· 20 23 (table) => [index("idx_pt_type").on(table.typeId), index("idx_pt_pokemon").on(table.pokemonId)], 21 24 ); 22 25 26 + export const pokemonTypesSelectSchema = createSelectSchema(pokemonTypes); 27 + 23 28 export const types = pgTable("types", { 24 29 id: integer().primaryKey(), 25 30 name: text().notNull(), 26 31 }); 32 + 33 + export const typesSelectSchema = createSelectSchema(types);
+2
src/env.ts
··· 4 4 export const env = createEnv({ 5 5 server: { 6 6 DATABASE_URL: v.pipe(v.string(), v.url(), v.minLength(1)), 7 + ELECTRIC_SOURCE: v.pipe(v.string(), v.minLength(1)), 8 + ELECTRIC_SECRET: v.pipe(v.string(), v.minLength(1)), 7 9 }, 8 10 9 11 /**
+40
src/lib/collections.ts
··· 1 + import { BasicIndex, createCollection } from "@tanstack/react-db"; 2 + import { electricCollectionOptions } from "@tanstack/electric-db-collection"; 3 + 4 + import { pokemonSelectSchema, typesSelectSchema, pokemonTypesSelectSchema } from "~/data/schema"; 5 + 6 + export const pokemonCollection = createCollection( 7 + electricCollectionOptions({ 8 + shapeOptions: { 9 + url: "http://localhost:3000/api/shapes/pokemon", 10 + }, 11 + getKey: (item) => item.id, 12 + schema: pokemonSelectSchema, 13 + }), 14 + ); 15 + pokemonCollection.createIndex((row) => row.id, { indexType: BasicIndex }); 16 + pokemonCollection.createIndex((row) => row.dexId, { indexType: BasicIndex }); 17 + 18 + export const typesCollection = createCollection( 19 + electricCollectionOptions({ 20 + shapeOptions: { 21 + url: "http://localhost:3000/api/shapes/types", 22 + }, 23 + getKey: (item) => item.id, 24 + schema: typesSelectSchema, 25 + }), 26 + ); 27 + typesCollection.createIndex((row) => row.id, { indexType: BasicIndex }); 28 + 29 + export const pokemonTypesCollection = createCollection( 30 + electricCollectionOptions({ 31 + shapeOptions: { 32 + url: "http://localhost:3000/api/shapes/pokemon-types", 33 + }, 34 + getKey: (item) => item.id, 35 + schema: pokemonTypesSelectSchema, 36 + }), 37 + ); 38 + pokemonTypesCollection.createIndex((row) => row.id, { indexType: BasicIndex }); 39 + pokemonTypesCollection.createIndex((row) => row.pokemonId, { indexType: BasicIndex }); 40 + pokemonTypesCollection.createIndex((row) => row.typeId, { indexType: BasicIndex });
+84
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 LiveQueryRouteImport } from './routes/live-query' 14 15 import { Route as IntentPreloadingRouteImport } from './routes/intent-preloading' 15 16 import { Route as FiltersRouteImport } from './routes/filters' 16 17 import { Route as DebouncedPreloadFiltersRouteImport } from './routes/debounced-preload-filters' 17 18 import { Route as BasicRouteImport } from './routes/basic' 18 19 import { Route as IndexRouteImport } from './routes/index' 20 + import { Route as ApiShapesTypesRouteImport } from './routes/api/shapes/types' 21 + import { Route as ApiShapesPokemonTypesRouteImport } from './routes/api/shapes/pokemon-types' 22 + import { Route as ApiShapesPokemonRouteImport } from './routes/api/shapes/pokemon' 19 23 20 24 const PreloadingRoute = PreloadingRouteImport.update({ 21 25 id: '/preloading', ··· 25 29 const PaginationRoute = PaginationRouteImport.update({ 26 30 id: '/pagination', 27 31 path: '/pagination', 32 + getParentRoute: () => rootRouteImport, 33 + } as any) 34 + const LiveQueryRoute = LiveQueryRouteImport.update({ 35 + id: '/live-query', 36 + path: '/live-query', 28 37 getParentRoute: () => rootRouteImport, 29 38 } as any) 30 39 const IntentPreloadingRoute = IntentPreloadingRouteImport.update({ ··· 52 61 path: '/', 53 62 getParentRoute: () => rootRouteImport, 54 63 } as any) 64 + const ApiShapesTypesRoute = ApiShapesTypesRouteImport.update({ 65 + id: '/api/shapes/types', 66 + path: '/api/shapes/types', 67 + getParentRoute: () => rootRouteImport, 68 + } as any) 69 + const ApiShapesPokemonTypesRoute = ApiShapesPokemonTypesRouteImport.update({ 70 + id: '/api/shapes/pokemon-types', 71 + path: '/api/shapes/pokemon-types', 72 + getParentRoute: () => rootRouteImport, 73 + } as any) 74 + const ApiShapesPokemonRoute = ApiShapesPokemonRouteImport.update({ 75 + id: '/api/shapes/pokemon', 76 + path: '/api/shapes/pokemon', 77 + getParentRoute: () => rootRouteImport, 78 + } as any) 55 79 56 80 export interface FileRoutesByFullPath { 57 81 '/': typeof IndexRoute ··· 59 83 '/debounced-preload-filters': typeof DebouncedPreloadFiltersRoute 60 84 '/filters': typeof FiltersRoute 61 85 '/intent-preloading': typeof IntentPreloadingRoute 86 + '/live-query': typeof LiveQueryRoute 62 87 '/pagination': typeof PaginationRoute 63 88 '/preloading': typeof PreloadingRoute 89 + '/api/shapes/pokemon': typeof ApiShapesPokemonRoute 90 + '/api/shapes/pokemon-types': typeof ApiShapesPokemonTypesRoute 91 + '/api/shapes/types': typeof ApiShapesTypesRoute 64 92 } 65 93 export interface FileRoutesByTo { 66 94 '/': typeof IndexRoute ··· 68 96 '/debounced-preload-filters': typeof DebouncedPreloadFiltersRoute 69 97 '/filters': typeof FiltersRoute 70 98 '/intent-preloading': typeof IntentPreloadingRoute 99 + '/live-query': typeof LiveQueryRoute 71 100 '/pagination': typeof PaginationRoute 72 101 '/preloading': typeof PreloadingRoute 102 + '/api/shapes/pokemon': typeof ApiShapesPokemonRoute 103 + '/api/shapes/pokemon-types': typeof ApiShapesPokemonTypesRoute 104 + '/api/shapes/types': typeof ApiShapesTypesRoute 73 105 } 74 106 export interface FileRoutesById { 75 107 __root__: typeof rootRouteImport ··· 78 110 '/debounced-preload-filters': typeof DebouncedPreloadFiltersRoute 79 111 '/filters': typeof FiltersRoute 80 112 '/intent-preloading': typeof IntentPreloadingRoute 113 + '/live-query': typeof LiveQueryRoute 81 114 '/pagination': typeof PaginationRoute 82 115 '/preloading': typeof PreloadingRoute 116 + '/api/shapes/pokemon': typeof ApiShapesPokemonRoute 117 + '/api/shapes/pokemon-types': typeof ApiShapesPokemonTypesRoute 118 + '/api/shapes/types': typeof ApiShapesTypesRoute 83 119 } 84 120 export interface FileRouteTypes { 85 121 fileRoutesByFullPath: FileRoutesByFullPath ··· 89 125 | '/debounced-preload-filters' 90 126 | '/filters' 91 127 | '/intent-preloading' 128 + | '/live-query' 92 129 | '/pagination' 93 130 | '/preloading' 131 + | '/api/shapes/pokemon' 132 + | '/api/shapes/pokemon-types' 133 + | '/api/shapes/types' 94 134 fileRoutesByTo: FileRoutesByTo 95 135 to: 96 136 | '/' ··· 98 138 | '/debounced-preload-filters' 99 139 | '/filters' 100 140 | '/intent-preloading' 141 + | '/live-query' 101 142 | '/pagination' 102 143 | '/preloading' 144 + | '/api/shapes/pokemon' 145 + | '/api/shapes/pokemon-types' 146 + | '/api/shapes/types' 103 147 id: 104 148 | '__root__' 105 149 | '/' ··· 107 151 | '/debounced-preload-filters' 108 152 | '/filters' 109 153 | '/intent-preloading' 154 + | '/live-query' 110 155 | '/pagination' 111 156 | '/preloading' 157 + | '/api/shapes/pokemon' 158 + | '/api/shapes/pokemon-types' 159 + | '/api/shapes/types' 112 160 fileRoutesById: FileRoutesById 113 161 } 114 162 export interface RootRouteChildren { ··· 117 165 DebouncedPreloadFiltersRoute: typeof DebouncedPreloadFiltersRoute 118 166 FiltersRoute: typeof FiltersRoute 119 167 IntentPreloadingRoute: typeof IntentPreloadingRoute 168 + LiveQueryRoute: typeof LiveQueryRoute 120 169 PaginationRoute: typeof PaginationRoute 121 170 PreloadingRoute: typeof PreloadingRoute 171 + ApiShapesPokemonRoute: typeof ApiShapesPokemonRoute 172 + ApiShapesPokemonTypesRoute: typeof ApiShapesPokemonTypesRoute 173 + ApiShapesTypesRoute: typeof ApiShapesTypesRoute 122 174 } 123 175 124 176 declare module '@tanstack/react-router' { ··· 137 189 preLoaderRoute: typeof PaginationRouteImport 138 190 parentRoute: typeof rootRouteImport 139 191 } 192 + '/live-query': { 193 + id: '/live-query' 194 + path: '/live-query' 195 + fullPath: '/live-query' 196 + preLoaderRoute: typeof LiveQueryRouteImport 197 + parentRoute: typeof rootRouteImport 198 + } 140 199 '/intent-preloading': { 141 200 id: '/intent-preloading' 142 201 path: '/intent-preloading' ··· 172 231 preLoaderRoute: typeof IndexRouteImport 173 232 parentRoute: typeof rootRouteImport 174 233 } 234 + '/api/shapes/types': { 235 + id: '/api/shapes/types' 236 + path: '/api/shapes/types' 237 + fullPath: '/api/shapes/types' 238 + preLoaderRoute: typeof ApiShapesTypesRouteImport 239 + parentRoute: typeof rootRouteImport 240 + } 241 + '/api/shapes/pokemon-types': { 242 + id: '/api/shapes/pokemon-types' 243 + path: '/api/shapes/pokemon-types' 244 + fullPath: '/api/shapes/pokemon-types' 245 + preLoaderRoute: typeof ApiShapesPokemonTypesRouteImport 246 + parentRoute: typeof rootRouteImport 247 + } 248 + '/api/shapes/pokemon': { 249 + id: '/api/shapes/pokemon' 250 + path: '/api/shapes/pokemon' 251 + fullPath: '/api/shapes/pokemon' 252 + preLoaderRoute: typeof ApiShapesPokemonRouteImport 253 + parentRoute: typeof rootRouteImport 254 + } 175 255 } 176 256 } 177 257 ··· 181 261 DebouncedPreloadFiltersRoute: DebouncedPreloadFiltersRoute, 182 262 FiltersRoute: FiltersRoute, 183 263 IntentPreloadingRoute: IntentPreloadingRoute, 264 + LiveQueryRoute: LiveQueryRoute, 184 265 PaginationRoute: PaginationRoute, 185 266 PreloadingRoute: PreloadingRoute, 267 + ApiShapesPokemonRoute: ApiShapesPokemonRoute, 268 + ApiShapesPokemonTypesRoute: ApiShapesPokemonTypesRoute, 269 + ApiShapesTypesRoute: ApiShapesTypesRoute, 186 270 } 187 271 export const routeTree = rootRouteImport 188 272 ._addFileChildren(rootRouteChildren)
+40
src/routes/api/shapes/pokemon-types.ts
··· 1 + import { createFileRoute } from "@tanstack/react-router"; 2 + import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from "@electric-sql/client"; 3 + 4 + import { env } from "~/env.js"; 5 + 6 + export const Route = createFileRoute("/api/shapes/pokemon-types")({ 7 + server: { 8 + handlers: { 9 + GET: async ({ request }) => { 10 + let electricUrl = new URL(`https://api.electric-sql.cloud/v1/shape`); 11 + const query = new URL(request.url).searchParams; 12 + 13 + // Forward only Electric protocol parameters 14 + ELECTRIC_PROTOCOL_QUERY_PARAMS.forEach((param) => { 15 + if (query.get(param)) { 16 + electricUrl.searchParams.set(param, query.get(param)!); 17 + } 18 + }); 19 + 20 + // Server controls table and authorization 21 + electricUrl.searchParams.set("table", "pokemon_types"); 22 + electricUrl.searchParams.append("source_id", env.ELECTRIC_SOURCE); 23 + electricUrl.searchParams.append("secret", env.ELECTRIC_SECRET); 24 + 25 + // Proxy response with streaming... 26 + const response = await fetch(electricUrl); 27 + 28 + const headers = new Headers(response.headers); 29 + headers.delete(`content-encoding`); 30 + headers.delete(`content-length`); 31 + 32 + return new Response(response.body, { 33 + status: response.status, 34 + statusText: response.statusText, 35 + headers, 36 + }); 37 + }, 38 + }, 39 + }, 40 + });
+40
src/routes/api/shapes/pokemon.ts
··· 1 + import { createFileRoute } from "@tanstack/react-router"; 2 + import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from "@electric-sql/client"; 3 + 4 + import { env } from "~/env.js"; 5 + 6 + export const Route = createFileRoute("/api/shapes/pokemon")({ 7 + server: { 8 + handlers: { 9 + GET: async ({ request }) => { 10 + let electricUrl = new URL(`https://api.electric-sql.cloud/v1/shape`); 11 + const query = new URL(request.url).searchParams; 12 + 13 + // Forward only Electric protocol parameters 14 + ELECTRIC_PROTOCOL_QUERY_PARAMS.forEach((param) => { 15 + if (query.get(param)) { 16 + electricUrl.searchParams.set(param, query.get(param)!); 17 + } 18 + }); 19 + 20 + // Server controls table and authorization 21 + electricUrl.searchParams.set("table", "pokemon"); 22 + electricUrl.searchParams.append("source_id", env.ELECTRIC_SOURCE); 23 + electricUrl.searchParams.append("secret", env.ELECTRIC_SECRET); 24 + 25 + // Proxy response with streaming... 26 + const response = await fetch(electricUrl); 27 + 28 + const headers = new Headers(response.headers); 29 + headers.delete(`content-encoding`); 30 + headers.delete(`content-length`); 31 + 32 + return new Response(response.body, { 33 + status: response.status, 34 + statusText: response.statusText, 35 + headers, 36 + }); 37 + }, 38 + }, 39 + }, 40 + });
+40
src/routes/api/shapes/types.ts
··· 1 + import { createFileRoute } from "@tanstack/react-router"; 2 + import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from "@electric-sql/client"; 3 + 4 + import { env } from "~/env.js"; 5 + 6 + export const Route = createFileRoute("/api/shapes/types")({ 7 + server: { 8 + handlers: { 9 + GET: async ({ request }) => { 10 + let electricUrl = new URL(`https://api.electric-sql.cloud/v1/shape`); 11 + const query = new URL(request.url).searchParams; 12 + 13 + // Forward only Electric protocol parameters 14 + ELECTRIC_PROTOCOL_QUERY_PARAMS.forEach((param) => { 15 + if (query.get(param)) { 16 + electricUrl.searchParams.set(param, query.get(param)!); 17 + } 18 + }); 19 + 20 + // Server controls table and authorization 21 + electricUrl.searchParams.set("table", "types"); 22 + electricUrl.searchParams.append("source_id", env.ELECTRIC_SOURCE); 23 + electricUrl.searchParams.append("secret", env.ELECTRIC_SECRET); 24 + 25 + // Proxy response with streaming... 26 + const response = await fetch(electricUrl); 27 + 28 + const headers = new Headers(response.headers); 29 + headers.delete(`content-encoding`); 30 + headers.delete(`content-length`); 31 + 32 + return new Response(response.body, { 33 + status: response.status, 34 + statusText: response.statusText, 35 + headers, 36 + }); 37 + }, 38 + }, 39 + }, 40 + });
+77
src/routes/live-query.tsx
··· 1 + import { Suspense } from "react"; 2 + import { createFileRoute } from "@tanstack/react-router"; 3 + import { useLiveSuspenseQuery, eq } from "@tanstack/react-db"; 4 + import * as v from "valibot"; 5 + import { PaginationNav } from "~/components/pagination-nav"; 6 + import { ConsoleCard } from "~/components/console/console-card"; 7 + import { SectionHeader } from "~/components/console/section-header"; 8 + import { PokemonTableSkeleton } from "~/components/console/pokemon-table-skeleton"; 9 + import { POKEMON_LIMIT } from "~/constants"; 10 + import { lazily } from "~/util/lazily"; 11 + import { pokemonCollection, typesCollection, pokemonTypesCollection } from "~/lib/collections"; 12 + 13 + const { PokemonTable } = lazily(() => import("~/components/console/pokemon-table")); 14 + 15 + const searchParamsSchema = v.object({ 16 + offset: v.optional(v.number(), 0), 17 + }); 18 + 19 + export const Route = createFileRoute("/live-query")({ 20 + ssr: false, 21 + validateSearch: searchParamsSchema, 22 + component: RouteComponent, 23 + }); 24 + 25 + function RouteComponent() { 26 + const { offset: currentOffset } = Route.useSearch(); 27 + 28 + return ( 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" /> 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> 50 + </div> 51 + </main> 52 + ); 53 + } 54 + 55 + function PokemonTableContent({ currentOffset }: { currentOffset: number }) { 56 + const { data } = useLiveSuspenseQuery( 57 + (q) => { 58 + return q 59 + .from({ 60 + pokemon: pokemonCollection, 61 + }) 62 + .orderBy(({ pokemon }) => pokemon.dexId) 63 + .offset(currentOffset) 64 + .limit(POKEMON_LIMIT); 65 + }, 66 + [currentOffset], 67 + ); 68 + console.log(data); 69 + 70 + return ( 71 + <> 72 + <div className="min-h-125"> 73 + <PokemonTable pokemon={data as any} /> 74 + </div> 75 + </> 76 + ); 77 + }