From 1efae8fd79c4a6e90f017ed691f74657864280cc Mon Sep 17 00:00:00 2001 From: Thad Miller Date: Sat, 28 Feb 2026 10:19:40 -0500 Subject: [PATCH] [feat] card grid is completely driven from search --- scripts/reindex.ts | 45 ++++++++++++++++++++++++++-- scripts/sync-variants.ts | 4 +-- src/pages/partials/card-modal.astro | 2 +- src/pages/partials/cards.astro | 46 ++++++++++------------------- 4 files changed, 62 insertions(+), 35 deletions(-) diff --git a/scripts/reindex.ts b/scripts/reindex.ts index b5b4518..78f9144 100644 --- a/scripts/reindex.ts +++ b/scripts/reindex.ts @@ -4,11 +4,16 @@ import { db, poolConnection } from '../src/db/index.ts'; import { client } from '../src/db/typesense.ts'; import { release } from 'node:os'; +const DollarToInt = (dollar: any) => { + if (dollar === null) return null; + return Math.round(dollar * 100); +} async function createCollection(client: Client) { // Delete the collection if it already exists to ensure a clean slate try { - const response = await client.collections('cards').delete(); + await client.collections('cards').delete(); + await client.collections('skus').delete(); //console.log(`Collection "cards" deleted successfully:`, response); } catch (error) { //console.error(`Error deleting collection "cards":`, error); @@ -23,6 +28,7 @@ async function createCollection(client: Client) { await client.collections().create({ name: 'cards', fields: [ + { name: 'id', type: 'string' }, { name: 'cardId', type: 'int32' }, { name: 'productId', type: 'int32' }, { name: 'variant', type: 'string', facet: true }, @@ -36,6 +42,7 @@ async function createCollection(client: Client) { { name: 'Artist', type: 'string' }, { name: 'sealed', type: 'bool' }, { name: 'releaseDate', type: 'int32'}, + { name: 'sku_id', type: 'string[]', optional: true, reference: 'skus.id', async_reference: true } ], //default_sorting_field: 'productId', }); @@ -45,18 +52,38 @@ async function createCollection(client: Client) { process.exit(1); } } + + try { + await client.collections('skus').retrieve(); + console.log(chalk.yellow('Collection "skus" already exists.')); + } catch(error) { + if (error instanceof Error && error.message.includes('404')) { + await client.collections().create({ + name: 'skus', + fields: [ + { name: 'id', type: 'string' }, + { name: 'condition', type: 'string' }, + { name: 'highestPrice', type: 'int32', optional: true }, + { name: 'lowestPrice', type: 'int32', optional: true }, + { name: 'marketPrice', type: 'int32', optional: true }, + //{ name: 'card_id', type: 'string', reference: 'cards.id' }, + ] + }); + } + } } async function preloadSearchIndex() { const pokemon = await db.query.cards.findMany({ - with: { set: true, tcgdata: true }, + with: { set: true, tcgdata: true, prices: true }, }); // Ensure the collection exists before importing documents await createCollection(client); await client.collections('cards').documents().import(pokemon.map(card => ({ + id: card.cardId.toString(), cardId: card.cardId, productId: card.productId, variant: card.variant, @@ -70,8 +97,22 @@ async function preloadSearchIndex() { Artist: card.Artist || "", sealed: card.sealed, releaseDate: card.tcgdata?.releaseDate ? Math.floor(new Date(card.tcgdata.releaseDate).getTime() / 1000) : 0, + sku_id: card.prices.map(price => price.skuId.toString()) })), { action: 'upsert' }); + const skus = await db.query.skus.findMany({ + with: { card: true } + }); + + await client.collections('skus').documents().import(skus.map(sku => ({ + id: sku.skuId.toString(), + condition: sku.condition, + highestPrice: DollarToInt(sku.highestPrice), + lowestPrice: DollarToInt(sku.lowestPrice), + marketPrice: DollarToInt(sku.marketPrice), + //card_id: sku.card?.cardId.toString() + }))); + console.log(chalk.green('Search index preloaded with Pokémon cards.')); } diff --git a/scripts/sync-variants.ts b/scripts/sync-variants.ts index 53c3fbf..d19ae4f 100644 --- a/scripts/sync-variants.ts +++ b/scripts/sync-variants.ts @@ -7,7 +7,7 @@ async function syncVariants() { join tcgcards t on c.productId = t.productId join (select distinct productId, variant from skus) b on c.productId = b.productId and c.variant = b.variant left join tcg_overrides o on c.productId = o.productId -set c.productName = coalesce(o.productName, regexp_replace(regexp_replace(regexp_replace(coalesce(nullif(t.productName, ''), t.productUrlName), ' \\[.*\\]', ''),' \\(.*\\)',''),' - .*$','')), +set c.productName = coalesce(o.productName, regexp_replace(regexp_replace(regexp_replace(coalesce(nullif(t.productName, ''), t.productUrlName), ' \\\\[.*\\\\]', ''),' \\\\(.*\\\\)',''),' - .*$','')), c.productLineName = coalesce(o.productLineName, t.productLineName), c.productUrlName = coalesce(o.productUrlName, t.productUrlName), c.rarityName = coalesce(o.rarityName, t.rarityName), c.sealed = coalesce(o.sealed, t.sealed), c.setId = coalesce(o.setId, t.setId), c.cardType = coalesce(o.cardType, t.cardType), c.energyType = coalesce(o.energyType, t.energyType), c.number = coalesce(o.number, t.number), c.Artist = coalesce(o.Artist, t.Artist)`); @@ -15,7 +15,7 @@ c.energyType = coalesce(o.energyType, t.energyType), c.number = coalesce(o.numbe const inserts = await db.execute(sql`insert into cards (productId, variant, productName, productLineName, productUrlName, rarityName, sealed, setId, cardType, energyType, number, Artist) select t.productId, b.variant, -coalesce(o.productName, regexp_replace(regexp_replace(regexp_replace(coalesce(nullif(t.productName, ''), t.productUrlName), ' \\[.*\\]', ''),' \\(.*\\)',''),' - .*$','')) as productName, +coalesce(o.productName, regexp_replace(regexp_replace(regexp_replace(coalesce(nullif(t.productName, ''), t.productUrlName), ' \\\\[.*\\\\]', ''),' \\\\(.*\\\\)',''),' - .*$','')) as productName, coalesce(o.productLineName, t.productLineName) as productLineName, coalesce(o.productUrlName, t.productUrlName) as productUrlName, coalesce(o.rarityName, t.rarityName) as rarityName, coalesce(o.sealed, t.sealed) as sealed, coalesce(o.setId, t.setId) as setId, coalesce(o.cardType, t.cardType) as cardType, coalesce(o.energyType, t.energyType) as energyType, coalesce(o.number, t.number) as number, coalesce(o.Artist, t.Artist) as Artist diff --git a/src/pages/partials/card-modal.astro b/src/pages/partials/card-modal.astro index 3409a5c..25c93af 100644 --- a/src/pages/partials/card-modal.astro +++ b/src/pages/partials/card-modal.astro @@ -3,7 +3,7 @@ import ebay from "/vendors/ebay.svg?raw"; import SetIcon from '../../components/SetIcon.astro'; import EnergyIcon from '../../components/EnergyIcon.astro'; import RarityIcon from '../../components/RarityIcon.astro'; -import { db } from '../../db/index.ts'; +import { db } from '../../db/index'; import { privateDecrypt } from "node:crypto"; export const partial = true; diff --git a/src/pages/partials/cards.astro b/src/pages/partials/cards.astro index 1f3644c..89daa85 100644 --- a/src/pages/partials/cards.astro +++ b/src/pages/partials/cards.astro @@ -1,7 +1,7 @@ --- import { client } from '../../db/typesense'; -import { db } from '../../db'; import RarityIcon from '../../components/RarityIcon.astro'; +import * as util from 'node:util'; export const prerender = false; @@ -55,6 +55,7 @@ let searchArray = [{ facet_by: Object.keys(facetFields).join(','), page: Math.floor(start / 20) + 1, sort_by: '_text_match:asc, releaseDate:desc, number:asc', + include_fields: '$skus(*)', }]; // on first load (start === 0) we want to get the facets for the filters @@ -66,7 +67,8 @@ if (start === 0) { per_page: 0, facet_by: facet, page: 1, - sort_by: '' + sort_by: '', + include_fields: '', }); } } @@ -79,28 +81,19 @@ const commonSearchParams = { // use typesense to search for cards matching the query and return the productIds of the results const searchResults = await client.multiSearch.perform(searchRequests, commonSearchParams); -//console.log(searchResults); +console.log(util.inspect(searchResults.results[0], {depth: null})); const cardResults = searchResults.results[0] as any; -const cardIds = cardResults.hits?.map((hit: any) => hit.document.cardId) ?? []; +const pokemon = cardResults.hits?.map((hit: any) => hit.document) ?? []; const totalHits = cardResults?.found; -//const facets = cardResults?.facet_counts; -// get pokemon data with prices and set info using searchResults and then query the database for each card to get the prices and set info -const pokemon = await db.query.cards.findMany({ - where: { cardId: { in: cardIds, }, }, - with: { - prices: true, - set: true, - } -}); // format price to 2 decimal places (or 0 if price >=100) and adds a $ sign, if the price is null it returns "–" -const formatPrice = (price:any) => { - if (price === null) { - return "—"; - } - price = Number(price); +const formatPrice = (condition:string, skus: any) => { + const sku = skus.find(price => price.condition === condition); + if (typeof sku === 'undefined' || typeof sku.marketPrice === 'undefined') return '—'; + + const price = Number(sku.marketPrice) / 100.0; if (price > 99.99) return `$${Math.round(price)}`; return `$${price.toFixed(2)}`; }; @@ -124,7 +117,6 @@ if (start === 0) { var facets = searchResults.results.slice(1).map((result: any) => { return result.facet_counts[0]; }); - console.log(facets); } --- @@ -162,17 +154,11 @@ if (start === 0) { {card.productName}
- {card.prices - .slice() - .sort((a, b) => conditionOrder.indexOf(a.condition) - conditionOrder.indexOf(b.condition)) - .filter((price, index, arr) => - arr.findIndex(p => p.condition === price.condition) === index - ) - .map((price) => ( -
- { conditionShort(price.condition) } -
{formatPrice(price.marketPrice)} -
+ {conditionOrder.map((condition) => ( +
+ { conditionShort(condition) } +
{formatPrice(condition, card.skus)} +
))}
{card.productName}