this repo has no description
0
fork

Configure Feed

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

listing module

+119 -57
+37
src/lib/pokemon-listing.test.ts
··· 1 + import { describe, expect, it } from "vite-plus/test"; 2 + import { POKEMON_LIMIT } from "~/constants"; 3 + import { normalizePokemonNameFilter, toPokemonListing } from "./pokemon-listing"; 4 + 5 + const makeRows = (count: number) => 6 + Array.from({ length: count }, (_, index) => ({ 7 + id: index + 1, 8 + name: `pokemon-${index + 1}`, 9 + types: [{ name: "normal" }], 10 + })); 11 + 12 + describe("pokemon listing", () => { 13 + it("shapes display rows and pagination from limit-plus-one results", () => { 14 + const listing = toPokemonListing(makeRows(POKEMON_LIMIT + 1), { offset: POKEMON_LIMIT }); 15 + 16 + expect(listing.pokemon).toHaveLength(POKEMON_LIMIT); 17 + expect(listing.prevOffset).toBe(0); 18 + expect(listing.nextOffset).toBe(POKEMON_LIMIT * 2); 19 + expect(listing.pokemon[0]).toEqual({ 20 + id: 1, 21 + name: "pokemon-1", 22 + types: [{ name: "normal" }], 23 + }); 24 + }); 25 + 26 + it("normalizes filtered listings without changing the caller's original filter value", () => { 27 + const listing = toPokemonListing(makeRows(1), { 28 + offset: 0, 29 + nameFilter: " pika ", 30 + }); 31 + 32 + expect(normalizePokemonNameFilter(" pika ")).toBe("pika"); 33 + expect(listing.appliedFilter).toBe("pika"); 34 + expect(listing.prevOffset).toBe(null); 35 + expect(listing.nextOffset).toBe(null); 36 + }); 37 + });
+49
src/lib/pokemon-listing.ts
··· 1 + import { POKEMON_LIMIT } from "~/constants"; 2 + 3 + export type PokemonListingRow = { 4 + id: number; 5 + name: string; 6 + types: Array<{ name: string }>; 7 + }; 8 + 9 + type PokemonListingSourceRow = { 10 + id: number; 11 + name: string; 12 + types: Array<{ name: string }>; 13 + }; 14 + 15 + export type PokemonListingResult = { 16 + pokemon: PokemonListingRow[]; 17 + nextOffset: number | null; 18 + prevOffset: number | null; 19 + appliedFilter?: string; 20 + }; 21 + 22 + export const normalizePokemonNameFilter = (nameFilter: string) => { 23 + return nameFilter.trim(); 24 + }; 25 + 26 + export const getPokemonListingQueryLimit = () => { 27 + return POKEMON_LIMIT + 1; 28 + }; 29 + 30 + export const toPokemonListing = ( 31 + rows: PokemonListingSourceRow[], 32 + options: { offset: number; nameFilter?: string }, 33 + ): PokemonListingResult => { 34 + const hasMore = rows.length > POKEMON_LIMIT; 35 + const pageRows = hasMore ? rows.slice(0, POKEMON_LIMIT) : rows; 36 + const appliedFilter = 37 + options.nameFilter === undefined ? undefined : normalizePokemonNameFilter(options.nameFilter); 38 + 39 + return { 40 + pokemon: pageRows.map((pokemon) => ({ 41 + id: pokemon.id, 42 + name: pokemon.name, 43 + types: pokemon.types.map((type) => ({ name: type.name })), 44 + })), 45 + nextOffset: hasMore ? options.offset + POKEMON_LIMIT : null, 46 + prevOffset: options.offset > 0 ? Math.max(0, options.offset - POKEMON_LIMIT) : null, 47 + ...(appliedFilter === undefined ? {} : { appliedFilter }), 48 + }; 49 + };
+11 -14
src/routes/live-query-filters.tsx
··· 15 15 pokemonTypesCollection, 16 16 typesCollection, 17 17 } from "~/data/local/collections"; 18 - import { POKEMON_LIMIT } from "~/constants"; 18 + import { 19 + getPokemonListingQueryLimit, 20 + normalizePokemonNameFilter, 21 + toPokemonListing, 22 + } from "~/lib/pokemon-listing"; 19 23 import { getStrategyArticle } from "~/server/strategy-article.functions"; 20 24 21 25 const searchParamsSchema = v.object({ ··· 112 116 currentOffset: number; 113 117 nameFilter: string; 114 118 }) { 115 - const trimmedNameFilter = nameFilter.trim(); 119 + const trimmedNameFilter = normalizePokemonNameFilter(nameFilter); 116 120 const { data } = useLiveSuspenseQuery( 117 121 (q) => { 118 122 let query = q.from({ pokemon: pokemonCollection }); ··· 124 128 return query 125 129 .orderBy(({ pokemon }) => pokemon.dexId) 126 130 .offset(currentOffset) 127 - .limit(POKEMON_LIMIT + 1) 131 + .limit(getPokemonListingQueryLimit()) 128 132 .select(({ pokemon }) => ({ 129 133 id: pokemon.id, 130 134 name: pokemon.name, ··· 145 149 [currentOffset, trimmedNameFilter], 146 150 ); 147 151 148 - const hasMore = data.length > POKEMON_LIMIT; 149 - const pokemon = (hasMore ? data.slice(0, POKEMON_LIMIT) : data).map((pokemon) => ({ 150 - id: pokemon.id, 151 - name: pokemon.name, 152 - types: pokemon.types.map((type) => ({ name: type.name })), 153 - })); 154 - const prevOffset = currentOffset > 0 ? Math.max(0, currentOffset - POKEMON_LIMIT) : null; 155 - const nextOffset = hasMore ? currentOffset + POKEMON_LIMIT : null; 152 + const listing = toPokemonListing(data, { offset: currentOffset, nameFilter }); 156 153 157 154 return ( 158 155 <PokedexTableResults 159 156 nameFilter={nameFilter} 160 - pokemon={pokemon} 157 + pokemon={listing.pokemon} 161 158 pagination={ 162 159 <PokedexPagination 163 160 nameFilter={nameFilter} 164 - prevOffset={prevOffset} 165 - nextOffset={nextOffset} 161 + prevOffset={listing.prevOffset} 162 + nextOffset={listing.nextOffset} 166 163 to="/live-query-filters" 167 164 /> 168 165 }
+9 -12
src/routes/live-query.tsx
··· 8 8 PokedexTableResults, 9 9 PokedexTableSection, 10 10 } from "~/components/tables/pokedex-table-section"; 11 - import { POKEMON_LIMIT } from "~/constants"; 12 11 import { getStrategyArticle } from "~/server/strategy-article.functions"; 13 12 import { 14 13 pokemonCollection, 15 14 typesCollection, 16 15 pokemonTypesCollection, 17 16 } from "~/data/local/collections"; 17 + import { getPokemonListingQueryLimit, toPokemonListing } from "~/lib/pokemon-listing"; 18 18 19 19 const searchParamsSchema = v.object({ 20 20 offset: v.optional(v.number(), 0), ··· 63 63 .from({ pokemon: pokemonCollection }) 64 64 .orderBy(({ pokemon }) => pokemon.dexId) 65 65 .offset(currentOffset) 66 - .limit(POKEMON_LIMIT + 1) 66 + .limit(getPokemonListingQueryLimit()) 67 67 .select(({ pokemon }) => ({ 68 68 id: pokemon.id, 69 69 name: pokemon.name, ··· 84 84 [currentOffset], 85 85 ); 86 86 87 - const hasMore = data.length > POKEMON_LIMIT; 88 - const pokemon = (hasMore ? data.slice(0, POKEMON_LIMIT) : data).map((pokemon) => ({ 89 - id: pokemon.id, 90 - name: pokemon.name, 91 - types: pokemon.types.map((type) => ({ name: type.name })), 92 - })); 93 - const prevOffset = currentOffset > 0 ? Math.max(0, currentOffset - POKEMON_LIMIT) : null; 94 - const nextOffset = hasMore ? currentOffset + POKEMON_LIMIT : null; 87 + const listing = toPokemonListing(data, { offset: currentOffset }); 95 88 96 89 return ( 97 90 <PokedexTableResults 98 - pokemon={pokemon} 91 + pokemon={listing.pokemon} 99 92 pagination={ 100 - <PokedexPagination prevOffset={prevOffset} nextOffset={nextOffset} to="/live-query" /> 93 + <PokedexPagination 94 + prevOffset={listing.prevOffset} 95 + nextOffset={listing.nextOffset} 96 + to="/live-query" 97 + /> 101 98 } 102 99 /> 103 100 );
+13 -31
src/server/pokemon.functions.ts
··· 1 1 import { createServerFn } from "@tanstack/react-start"; 2 2 import * as v from "valibot"; 3 3 import { DB } from "~/data/db"; 4 - import { POKEMON_LIMIT } from "~/constants"; 4 + import { 5 + getPokemonListingQueryLimit, 6 + normalizePokemonNameFilter, 7 + toPokemonListing, 8 + } from "~/lib/pokemon-listing"; 5 9 6 10 const PokemonListParamsSchema = v.object({ 7 11 offset: v.optional(v.number()), ··· 13 17 }); 14 18 15 19 const innerGetPokemonList = async (offset: number) => { 16 - // Fetch one extra item to check if there are more results 17 - const pokemon = await DB.queries.getPokemonAtOffset(offset, POKEMON_LIMIT + 1); 18 - 19 - // Check if there are more results by looking at the extra item 20 - const hasMore = pokemon.length > POKEMON_LIMIT; 21 - 22 - // Remove the extra item if it exists 23 - const results = hasMore ? pokemon.slice(0, -1) : pokemon; 20 + const pokemon = await DB.queries.getPokemonAtOffset(offset, getPokemonListingQueryLimit()); 24 21 25 - return { 26 - pokemon: results, 27 - nextOffset: hasMore ? offset + POKEMON_LIMIT : null, 28 - prevOffset: offset > 0 ? Math.max(0, offset - POKEMON_LIMIT) : null, 29 - }; 22 + return toPokemonListing(pokemon, { offset }); 30 23 }; 31 24 32 25 const innerGetFilteredPokemonList = async (offset: number, nameFilter: string) => { 33 - // If no filter is provided, fall back to regular query 34 - if (!nameFilter.trim()) { 26 + const appliedFilter = normalizePokemonNameFilter(nameFilter); 27 + 28 + if (!appliedFilter) { 35 29 return await innerGetPokemonList(offset); 36 30 } 37 31 38 - // Fetch one extra item to check if there are more results 39 32 const pokemon = await DB.queries.getFilteredPokemonAtOffset( 40 33 offset, 41 - POKEMON_LIMIT + 1, 42 - `%${nameFilter.trim()}%`, 34 + getPokemonListingQueryLimit(), 35 + appliedFilter, 43 36 ); 44 37 45 - // Check if there are more results by looking at the extra item 46 - const hasMore = pokemon.length > POKEMON_LIMIT; 47 - 48 - // Remove the extra item if it exists 49 - const results = hasMore ? pokemon.slice(0, -1) : pokemon; 50 - 51 - return { 52 - pokemon: results, 53 - nextOffset: hasMore ? offset + POKEMON_LIMIT : null, 54 - prevOffset: offset > 0 ? Math.max(0, offset - POKEMON_LIMIT) : null, 55 - appliedFilter: nameFilter.trim(), 56 - }; 38 + return toPokemonListing(pokemon, { offset, nameFilter: appliedFilter }); 57 39 }; 58 40 59 41 export const getServerPokemonList = createServerFn({ method: "GET" })