this repo has no description
0
fork

Configure Feed

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

well it's broke now

+182 -33
+47 -1
src/data/db.ts
··· 1 - import { sql } from "drizzle-orm"; 1 + import { SQL, like, sql } from "drizzle-orm"; 2 2 import { drizzle } from "drizzle-orm/libsql"; 3 3 import * as schema from "./schema"; 4 4 ··· 30 30 }) 31 31 .prepare(); 32 32 33 + const preparedGetFilteredPokemonAtOffset = db.query.pokemon 34 + .findMany({ 35 + columns: { 36 + id: true, 37 + name: true, 38 + dexId: true, 39 + }, 40 + where: (pokemon, { ilike }) => 41 + ilike(pokemon.name, sql.placeholder("nameFilter")), 42 + orderBy: (pokemon, { asc }) => [asc(pokemon.dexId)], 43 + limit: sql.placeholder("limit"), 44 + offset: sql.placeholder("offset"), 45 + with: { 46 + types: { 47 + orderBy: (types, { asc }) => [asc(types.typeId)], 48 + columns: {}, 49 + with: { 50 + type: { 51 + columns: { 52 + name: true, 53 + }, 54 + }, 55 + }, 56 + }, 57 + }, 58 + }) 59 + .prepare(); 60 + 33 61 export const DB = { 34 62 queries: { 35 63 getPokemonAtOffset: async (offset: number, limit: number) => { ··· 38 66 offset, 39 67 }); 40 68 }, 69 + getFilteredPokemonAtOffset: async ( 70 + offset: number, 71 + limit: number, 72 + nameFilter: string, 73 + ) => { 74 + // Convert the filter to a SQL LIKE pattern (case-insensitive) 75 + const likePattern = `%${nameFilter.toLowerCase()}%`; 76 + return preparedGetFilteredPokemonAtOffset.execute({ 77 + limit, 78 + offset, 79 + nameFilter: likePattern, 80 + }); 81 + }, 41 82 }, 42 83 }; 84 + 85 + // custom lower function 86 + export function lower(email: AnySQLiteColumn): SQL { 87 + return sql`lower(${email})`; 88 + }
+68 -32
src/routes/filters.tsx
··· 4 4 useSuspenseQuery, 5 5 } from "@tanstack/react-query"; 6 6 import { useServerFn } from "@tanstack/react-start"; 7 + import { useCallback, useState } from "react"; 7 8 import * as v from "valibot"; 8 9 import { PaginationNav } from "~/components/pagination-nav"; 9 10 import { Input } from "~/components/ui/input"; ··· 18 19 } from "~/components/ui/table"; 19 20 import { 20 21 POKEMON_LIMIT, 21 - getPokemonListQueryKey, 22 - getServerPokemonListQueryFn, 22 + getFilteredPokemonListQueryKey, 23 + getServerFilteredPokemonListQueryFn, 23 24 } from "~/util/pokemon"; 24 25 25 26 const searchParamsSchema = v.object({ ··· 34 35 name: search.name, 35 36 }), 36 37 context: ({ deps }) => { 37 - const newKey = getPokemonListQueryKey("filters", deps.offset); 38 + const newKey = getFilteredPokemonListQueryKey( 39 + "filters", 40 + deps.offset, 41 + deps.name, 42 + ); 38 43 39 44 const pokemonListOptions = queryOptions({ 40 45 queryKey: newKey, 41 - queryFn: getServerPokemonListQueryFn, 46 + queryFn: getServerFilteredPokemonListQueryFn, 42 47 }); 43 48 44 49 return { ··· 53 58 54 59 function RouteComponent() { 55 60 const { offset: currentOffset, name: nameFilter } = Route.useSearch(); 61 + const navigate = Route.useNavigate(); 56 62 const { pokemonListOptions: serverPokemonListOptions } = 57 63 Route.useRouteContext(); 58 64 const queryClient = useQueryClient(); 59 65 60 66 const { data } = useSuspenseQuery({ 61 67 ...serverPokemonListOptions, 62 - queryFn: useServerFn(getServerPokemonListQueryFn), 68 + queryFn: useServerFn(getServerFilteredPokemonListQueryFn), 63 69 }); 64 70 65 71 if (data.prevOffset !== null) { 66 72 void queryClient.prefetchQuery({ 67 73 ...serverPokemonListOptions, 68 - queryKey: getPokemonListQueryKey("filters", data.prevOffset), 74 + queryKey: getFilteredPokemonListQueryKey( 75 + "filters", 76 + data.prevOffset, 77 + nameFilter, 78 + ), 69 79 }); 70 80 } 71 81 72 82 if (data.nextOffset !== null) { 73 83 void queryClient.prefetchQuery({ 74 84 ...serverPokemonListOptions, 75 - queryKey: getPokemonListQueryKey("filters", data.nextOffset), 85 + queryKey: getFilteredPokemonListQueryKey( 86 + "filters", 87 + data.nextOffset, 88 + nameFilter, 89 + ), 76 90 }); 77 91 } 78 92 79 - // Filter Pokemon by name (dummy implementation for now) 80 - const filteredPokemon = data.pokemon.filter((pokemon) => 81 - pokemon.name.toLowerCase().includes(nameFilter.toLowerCase()), 82 - ); 93 + // Use the filtered results directly from the server 94 + const filteredPokemon = data.pokemon; 83 95 84 96 return ( 85 97 <div className="p-4"> ··· 91 103 {/* Filter UI */} 92 104 <div className="mb-6 p-4 border rounded-lg bg-gray-50"> 93 105 <h2 className="text-lg font-semibold mb-3">Filters</h2> 94 - <div className="space-y-4"> 95 - <div> 96 - <Label htmlFor="name-filter" className="text-sm font-medium"> 97 - Filter by Name 98 - </Label> 99 - <Input 100 - id="name-filter" 101 - type="text" 102 - placeholder="Enter Pokemon name..." 103 - value={nameFilter} 104 - onChange={(e) => { 105 - // This is dummy UI - in a real implementation, this would update the URL search params 106 - console.log("Filter changed:", e.target.value); 107 - }} 108 - className="mt-1" 109 - /> 110 - <p className="text-xs text-gray-500 mt-1"> 111 - Current filter: "{nameFilter}" (dummy UI - not functional yet) 112 - </p> 113 - </div> 114 - </div> 106 + <FilterForm 107 + key={`filter-form-${nameFilter}`} 108 + handleSubmit={(nameFilter) => { 109 + navigate({ 110 + search: { name: nameFilter }, 111 + }); 112 + }} 113 + initialName={nameFilter} 114 + /> 115 115 </div> 116 116 117 117 <Table> ··· 156 156 </div> 157 157 ); 158 158 } 159 + 160 + function FilterForm(props: { 161 + handleSubmit: (nameFilter: string) => void; 162 + initialName: string; 163 + }) { 164 + const [nameFilter, setNameFilter] = useState(props.initialName); 165 + 166 + const onSubmit = useCallback( 167 + (e: React.FormEvent<HTMLFormElement>) => { 168 + e.preventDefault(); 169 + props.handleSubmit(nameFilter); 170 + }, 171 + [nameFilter, props], 172 + ); 173 + 174 + return ( 175 + <div className="space-y-4"> 176 + <form onSubmit={onSubmit}> 177 + <Label htmlFor="name-filter" className="text-sm font-medium"> 178 + Filter by Name 179 + </Label> 180 + <Input 181 + id="name-filter" 182 + type="text" 183 + placeholder="Enter Pokemon name..." 184 + value={nameFilter} 185 + onChange={(e) => setNameFilter(e.target.value)} 186 + className="mt-1" 187 + /> 188 + <p className="text-xs text-gray-500 mt-1"> 189 + Current filter: "{nameFilter}" (dummy UI - not functional yet) 190 + </p> 191 + </form> 192 + </div> 193 + ); 194 + }
+67
src/util/pokemon.ts
··· 9 9 offset: v.optional(v.number()), 10 10 }); 11 11 12 + const FilteredPokemonListParamsSchema = v.object({ 13 + offset: v.optional(v.number()), 14 + nameFilter: v.optional(v.string()), 15 + }); 16 + 12 17 const innerGetPokemonList = async (offset: number) => { 13 18 // Fetch one extra item to check if there are more results 14 19 const pokemon = await DB.queries.getPokemonAtOffset( ··· 31 36 }; 32 37 }; 33 38 39 + const innerGetFilteredPokemonList = async ( 40 + offset: number, 41 + nameFilter: string, 42 + ) => { 43 + // If no filter is provided, fall back to regular query 44 + if (!nameFilter.trim()) { 45 + return await innerGetPokemonList(offset); 46 + } 47 + 48 + // Fetch one extra item to check if there are more results 49 + const pokemon = await DB.queries.getFilteredPokemonAtOffset( 50 + offset, 51 + POKEMON_LIMIT + 1, 52 + `%${nameFilter.trim()}%`, 53 + ); 54 + 55 + // Check if there are more results by looking at the extra item 56 + const hasMore = pokemon.length > POKEMON_LIMIT; 57 + 58 + // Remove the extra item if it exists 59 + const results = hasMore ? pokemon.slice(0, -1) : pokemon; 60 + 61 + console.log("filtered results", results, "filter:", nameFilter); 62 + 63 + return { 64 + pokemon: results, 65 + nextOffset: hasMore ? offset + POKEMON_LIMIT : null, 66 + prevOffset: offset > 0 ? Math.max(0, offset - POKEMON_LIMIT) : null, 67 + appliedFilter: nameFilter.trim(), 68 + }; 69 + }; 70 + 34 71 const getServerPokemonList = createServerFn({ method: "GET" }) 35 72 .validator((params) => { 36 73 const validated = v.parse(PokemonListParamsSchema, params); ··· 45 82 return await innerGetPokemonList(data.offset); 46 83 }); 47 84 85 + const getServerFilteredPokemonList = createServerFn({ method: "GET" }) 86 + .validator((params) => { 87 + const validated = v.parse(FilteredPokemonListParamsSchema, params); 88 + const offset = validated.offset ?? 0; 89 + const nameFilter = validated.nameFilter ?? ""; 90 + 91 + if (offset < 0) 92 + throw new Error("Offset must be greater than or equal to 0"); 93 + 94 + return { offset, nameFilter }; 95 + }) 96 + .handler(async ({ data }) => { 97 + return await innerGetFilteredPokemonList(data.offset, data.nameFilter); 98 + }); 99 + 48 100 export const getPokemonListQueryKey = (location: string, offset: number) => { 49 101 return ["pokemon-list", location, { offset }] as const; 50 102 }; 51 103 104 + export const getFilteredPokemonListQueryKey = ( 105 + location: string, 106 + offset: number, 107 + nameFilter: string, 108 + ) => { 109 + return ["pokemon-list", location, { offset, nameFilter }] as const; 110 + }; 111 + 52 112 export const getServerPokemonListQueryFn = ({ 53 113 queryKey, 54 114 }: QueryFunctionContext<ReturnType<typeof getPokemonListQueryKey>>) => { 55 115 const { offset } = queryKey[2]; 56 116 return getServerPokemonList({ data: { offset } }); 57 117 }; 118 + 119 + export const getServerFilteredPokemonListQueryFn = ({ 120 + queryKey, 121 + }: QueryFunctionContext<ReturnType<typeof getFilteredPokemonListQueryKey>>) => { 122 + const { offset, nameFilter } = queryKey[2]; 123 + return getServerFilteredPokemonList({ data: { offset, nameFilter } }); 124 + };