[feat] card grid is completely driven from search
This commit is contained in:
@@ -4,11 +4,16 @@ import { db, poolConnection } from '../src/db/index.ts';
|
|||||||
import { client } from '../src/db/typesense.ts';
|
import { client } from '../src/db/typesense.ts';
|
||||||
import { release } from 'node:os';
|
import { release } from 'node:os';
|
||||||
|
|
||||||
|
const DollarToInt = (dollar: any) => {
|
||||||
|
if (dollar === null) return null;
|
||||||
|
return Math.round(dollar * 100);
|
||||||
|
}
|
||||||
|
|
||||||
async function createCollection(client: Client) {
|
async function createCollection(client: Client) {
|
||||||
// Delete the collection if it already exists to ensure a clean slate
|
// Delete the collection if it already exists to ensure a clean slate
|
||||||
try {
|
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);
|
//console.log(`Collection "cards" deleted successfully:`, response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
//console.error(`Error deleting collection "cards":`, error);
|
//console.error(`Error deleting collection "cards":`, error);
|
||||||
@@ -23,6 +28,7 @@ async function createCollection(client: Client) {
|
|||||||
await client.collections().create({
|
await client.collections().create({
|
||||||
name: 'cards',
|
name: 'cards',
|
||||||
fields: [
|
fields: [
|
||||||
|
{ name: 'id', type: 'string' },
|
||||||
{ name: 'cardId', type: 'int32' },
|
{ name: 'cardId', type: 'int32' },
|
||||||
{ name: 'productId', type: 'int32' },
|
{ name: 'productId', type: 'int32' },
|
||||||
{ name: 'variant', type: 'string', facet: true },
|
{ name: 'variant', type: 'string', facet: true },
|
||||||
@@ -36,6 +42,7 @@ async function createCollection(client: Client) {
|
|||||||
{ name: 'Artist', type: 'string' },
|
{ name: 'Artist', type: 'string' },
|
||||||
{ name: 'sealed', type: 'bool' },
|
{ name: 'sealed', type: 'bool' },
|
||||||
{ name: 'releaseDate', type: 'int32'},
|
{ name: 'releaseDate', type: 'int32'},
|
||||||
|
{ name: 'sku_id', type: 'string[]', optional: true, reference: 'skus.id', async_reference: true }
|
||||||
],
|
],
|
||||||
//default_sorting_field: 'productId',
|
//default_sorting_field: 'productId',
|
||||||
});
|
});
|
||||||
@@ -45,18 +52,38 @@ async function createCollection(client: Client) {
|
|||||||
process.exit(1);
|
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() {
|
async function preloadSearchIndex() {
|
||||||
const pokemon = await db.query.cards.findMany({
|
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
|
// Ensure the collection exists before importing documents
|
||||||
await createCollection(client);
|
await createCollection(client);
|
||||||
|
|
||||||
await client.collections('cards').documents().import(pokemon.map(card => ({
|
await client.collections('cards').documents().import(pokemon.map(card => ({
|
||||||
|
id: card.cardId.toString(),
|
||||||
cardId: card.cardId,
|
cardId: card.cardId,
|
||||||
productId: card.productId,
|
productId: card.productId,
|
||||||
variant: card.variant,
|
variant: card.variant,
|
||||||
@@ -70,8 +97,22 @@ async function preloadSearchIndex() {
|
|||||||
Artist: card.Artist || "",
|
Artist: card.Artist || "",
|
||||||
sealed: card.sealed,
|
sealed: card.sealed,
|
||||||
releaseDate: card.tcgdata?.releaseDate ? Math.floor(new Date(card.tcgdata.releaseDate).getTime() / 1000) : 0,
|
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' });
|
})), { 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.'));
|
console.log(chalk.green('Search index preloaded with Pokémon cards.'));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ async function syncVariants() {
|
|||||||
join tcgcards t on c.productId = t.productId
|
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
|
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
|
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.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.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)`);
|
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)
|
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,
|
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.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.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
|
coalesce(o.energyType, t.energyType) as energyType, coalesce(o.number, t.number) as number, coalesce(o.Artist, t.Artist) as Artist
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import ebay from "/vendors/ebay.svg?raw";
|
|||||||
import SetIcon from '../../components/SetIcon.astro';
|
import SetIcon from '../../components/SetIcon.astro';
|
||||||
import EnergyIcon from '../../components/EnergyIcon.astro';
|
import EnergyIcon from '../../components/EnergyIcon.astro';
|
||||||
import RarityIcon from '../../components/RarityIcon.astro';
|
import RarityIcon from '../../components/RarityIcon.astro';
|
||||||
import { db } from '../../db/index.ts';
|
import { db } from '../../db/index';
|
||||||
import { privateDecrypt } from "node:crypto";
|
import { privateDecrypt } from "node:crypto";
|
||||||
|
|
||||||
export const partial = true;
|
export const partial = true;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
import { client } from '../../db/typesense';
|
import { client } from '../../db/typesense';
|
||||||
import { db } from '../../db';
|
|
||||||
import RarityIcon from '../../components/RarityIcon.astro';
|
import RarityIcon from '../../components/RarityIcon.astro';
|
||||||
|
import * as util from 'node:util';
|
||||||
|
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
@@ -55,6 +55,7 @@ let searchArray = [{
|
|||||||
facet_by: Object.keys(facetFields).join(','),
|
facet_by: Object.keys(facetFields).join(','),
|
||||||
page: Math.floor(start / 20) + 1,
|
page: Math.floor(start / 20) + 1,
|
||||||
sort_by: '_text_match:asc, releaseDate:desc, number:asc',
|
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
|
// on first load (start === 0) we want to get the facets for the filters
|
||||||
@@ -66,7 +67,8 @@ if (start === 0) {
|
|||||||
per_page: 0,
|
per_page: 0,
|
||||||
facet_by: facet,
|
facet_by: facet,
|
||||||
page: 1,
|
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
|
// use typesense to search for cards matching the query and return the productIds of the results
|
||||||
const searchResults = await client.multiSearch.perform(searchRequests, commonSearchParams);
|
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 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 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 "–"
|
// 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) => {
|
const formatPrice = (condition:string, skus: any) => {
|
||||||
if (price === null) {
|
const sku = skus.find(price => price.condition === condition);
|
||||||
return "—";
|
if (typeof sku === 'undefined' || typeof sku.marketPrice === 'undefined') return '—';
|
||||||
}
|
|
||||||
price = Number(price);
|
const price = Number(sku.marketPrice) / 100.0;
|
||||||
if (price > 99.99) return `$${Math.round(price)}`;
|
if (price > 99.99) return `$${Math.round(price)}`;
|
||||||
return `$${price.toFixed(2)}`;
|
return `$${price.toFixed(2)}`;
|
||||||
};
|
};
|
||||||
@@ -124,7 +117,6 @@ if (start === 0) {
|
|||||||
var facets = searchResults.results.slice(1).map((result: any) => {
|
var facets = searchResults.results.slice(1).map((result: any) => {
|
||||||
return result.facet_counts[0];
|
return result.facet_counts[0];
|
||||||
});
|
});
|
||||||
console.log(facets);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -162,17 +154,11 @@ if (start === 0) {
|
|||||||
<img src={`/cards/${card.productId}.jpg`} alt={card.productName} id="cardImage" loading="lazy" decoding="async" class="img-fluid rounded-4 mb-2 card-image image-grow w-100" onerror="this.onerror=null;this.src='/cards/default.jpg'"/>
|
<img src={`/cards/${card.productId}.jpg`} alt={card.productName} id="cardImage" loading="lazy" decoding="async" class="img-fluid rounded-4 mb-2 card-image image-grow w-100" onerror="this.onerror=null;this.src='/cards/default.jpg'"/>
|
||||||
</div>
|
</div>
|
||||||
<div class="row row-cols-5 gx-1 price-row mb-2">
|
<div class="row row-cols-5 gx-1 price-row mb-2">
|
||||||
{card.prices
|
{conditionOrder.map((condition) => (
|
||||||
.slice()
|
<div class="col price-label ps-1">
|
||||||
.sort((a, b) => conditionOrder.indexOf(a.condition) - conditionOrder.indexOf(b.condition))
|
{ conditionShort(condition) }
|
||||||
.filter((price, index, arr) =>
|
<br />{formatPrice(condition, card.skus)}
|
||||||
arr.findIndex(p => p.condition === price.condition) === index
|
</div>
|
||||||
)
|
|
||||||
.map((price) => (
|
|
||||||
<div class="col price-label ps-1">
|
|
||||||
{ conditionShort(price.condition) }
|
|
||||||
<br />{formatPrice(price.marketPrice)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="h5 my-0">{card.productName}</div>
|
<div class="h5 my-0">{card.productName}</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user