Files
pokemon/src/pages/partials/cards.astro

215 lines
8.8 KiB
Plaintext
Raw Normal View History

2026-02-17 13:07:29 -05:00
---
2026-02-25 23:20:45 -05:00
import { client } from '../../db/typesense';
2026-02-22 11:00:30 -05:00
import RarityIcon from '../../components/RarityIcon.astro';
2026-02-17 13:07:29 -05:00
2026-02-22 11:00:30 -05:00
export const prerender = false;
2026-02-27 15:27:10 -05:00
// all the facet fields we want to use for filtering
2026-03-01 12:57:06 -05:00
const facetFields:any = {
2026-02-27 15:27:10 -05:00
"productLineName": "Product Line",
"setName": "Set",
"variant": "Variant",
"rarityName": "Rarity",
"cardType": "Card Type",
"energyType": "Energy Type"
}
2026-02-22 11:00:30 -05:00
// get the query from post request using form data
const formData = await Astro.request.formData();
const query = formData.get('q')?.toString() || '';
2026-02-23 08:33:11 -05:00
const start = Number(formData.get('start')?.toString() || '0');
2026-02-26 15:51:00 -05:00
const filters = Array.from(formData.entries())
.filter(([key, value]) => key !== 'q' && key !== 'start')
.reduce((acc, [key, value]) => {
if (!acc[key]) {
acc[key] = [];
}
acc[key].push(value.toString());
return acc;
}, {} as Record<string, string[]>);
const filterChecked = (field: string, value: string) => {
return (filters[field]?.includes(value) ?? false) ? 'checked' : '';
};
const filterBy = Object.entries(filters).map(([field, values]) => {
return `${field}:=[${values.join(',')}]`;
}).join(' && ');
2026-02-22 11:00:30 -05:00
2026-02-27 15:27:10 -05:00
const facetFilter = (facet:string) => {
const otherFilters = Object.entries(filters)
.filter(([field]) => field !== facet)
.map(([field, values]) => `${field}:=[${values.join(',')}]`)
.join(' && ');
return `sealed:false${otherFilters ? ` && ${otherFilters}` : ''}`;
};
// primary search values (for cards)
let searchArray = [{
collection: 'cards',
2026-02-26 15:51:00 -05:00
filter_by: `sealed:false${filterBy ? ` && ${filterBy}` : ''}`,
2026-02-23 08:33:11 -05:00
per_page: 20,
2026-02-28 17:20:00 -05:00
facet_by: '',
max_facet_values: 0,
2026-02-23 08:33:11 -05:00
page: Math.floor(start / 20) + 1,
sort_by: '_text_match:asc, releaseDate:desc, number:asc',
include_fields: '$skus(*)',
2026-02-27 15:27:10 -05:00
}];
// on first load (start === 0) we want to get the facets for the filters
if (start === 0) {
for (const facet of Object.keys(facetFields)) {
searchArray.push({
collection: 'cards',
filter_by: facetFilter(facet),
per_page: 0,
facet_by: facet,
2026-02-28 17:20:00 -05:00
max_facet_values: 500,
2026-02-27 15:27:10 -05:00
page: 1,
sort_by: '',
include_fields: '',
2026-02-27 15:27:10 -05:00
});
}
}
const searchRequests = { searches: searchArray };
const commonSearchParams = {
q: query,
query_by: 'productLineName,productName,setName,number,rarityName,Artist',
};
// 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 cardResults = searchResults.results[0] as any;
const pokemon = cardResults.hits?.map((hit: any) => hit.document) ?? [];
2026-02-27 15:27:10 -05:00
const totalHits = cardResults?.found;
2026-02-17 13:07:29 -05:00
2026-02-22 11:00:30 -05:00
// format price to 2 decimal places (or 0 if price >=100) and adds a $ sign, if the price is null it returns ""
const formatPrice = (condition:string, skus: any) => {
2026-03-01 12:57:06 -05:00
const sku:any = skus.find((price:any) => price.condition === condition);
if (typeof sku === 'undefined' || typeof sku.marketPrice === 'undefined') return '—';
const price = Number(sku.marketPrice) / 100.0;
2026-02-22 11:00:30 -05:00
if (price > 99.99) return `$${Math.round(price)}`;
return `$${price.toFixed(2)}`;
2026-02-17 13:07:29 -05:00
};
2026-02-22 11:00:30 -05:00
const conditionOrder = ["Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged"];
2026-02-25 23:20:45 -05:00
const conditionShort = (condition:string) => {
return {
"Near Mint": "NM",
"Lightly Played": "LP",
"Moderately Played": "MP",
"Heavily Played": "HP",
"Damaged": "DMG"
}[condition] || condition.split(' ').map((w) => w[0]).join('');
}
2026-02-25 14:12:11 -05:00
2026-02-26 15:51:00 -05:00
const facetNames = (name:string) => {
2026-02-27 15:27:10 -05:00
return facetFields[name] || name;
}
2026-03-01 12:57:06 -05:00
const facets = searchResults.results.slice(1).map((result: any) => {
return result.facet_counts[0];
});
2026-02-26 15:51:00 -05:00
2026-02-17 13:07:29 -05:00
---
2026-02-23 08:33:11 -05:00
{(start === 0) &&
2026-02-26 15:51:00 -05:00
<div id="facetContainer" hx-swap-oob="true">
<div class="bg-dark sticky-top p-2 d-flex justify-content-end align-items-center">
2026-03-02 11:48:10 -05:00
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-danger me-2" id="clear-filters">Clear</button>
<button type="submit" form="searchform" data-bs-dismiss="offcanvas" class="btn btn-success">Apply Filters</button>
</div>
2026-02-26 15:51:00 -05:00
{facets.map((facet) => (
<div class="mt-2 mb-4 facet-group row align-items-center justify-content-between">
<div class="fs-5 m-0 col-auto pb-1 border-bottom border-light-subtle">{facetNames(facet.field_name)}</div>
2026-02-28 17:20:00 -05:00
{(facet.counts.length > 20) &&
<input class="facet-filter form-control col-auto me-3" type="text" id={`filter_${facet.field_name}`} placeholder="Search..." />
2026-02-28 17:20:00 -05:00
}
<div class="facet-list col-11 mt-2">
2026-03-01 12:57:06 -05:00
{facet.counts.map((count:any) => (
<div class="facet-item form-check form-switch" data-facet-value={count.value.toLowerCase()}>
<label class="form-check-label fs-7">
2026-02-28 17:20:00 -05:00
<input type="checkbox" name={facet.field_name} value={count.value} checked={filterChecked(facet.field_name, count.value)} class="form-check-input" form="searchform" />
{count.value} <span class="badge text-bg-light align-baseline">{count.count}</span>
2026-02-28 17:20:00 -05:00
</label>
</div>
))}
</div>
2026-02-26 15:51:00 -05:00
</div>
))}
</div>
2026-02-28 17:20:00 -05:00
<script define:vars={{ totalHits, filters, facets }} is:inline>
2026-03-02 11:48:10 -05:00
// Filter the facet values to make things like Set easier to find
2026-02-28 17:20:00 -05:00
const facetfilters = document.querySelectorAll('.facet-filter');
for (const facetfilter of facetfilters) {
const facetgroup = facetfilter.closest('.facet-group');
const facetlist = facetgroup.querySelector('.facet-list');
const facetitems = facetlist.querySelectorAll('.facet-item');
facetfilter.addEventListener('input', (e) => {
const match = e.target.value.toLowerCase();
2026-03-02 11:48:10 -05:00
for (const item of facetitems) {
2026-02-28 17:20:00 -05:00
const text = item.getAttribute('data-facet-value');
if (text.includes(match)) item.style.display = ''; // Show
else item.style.display = 'none'; // Hide
}
});
}
2026-03-02 11:48:10 -05:00
// Clear all facets and resubmit the form with only the text box
const clearfilters = document.getElementById('clear-filters');
clearfilters.addEventListener('click', (e) => {
const facetContainer = clearfilters.closest('#facetContainer');
for (const item of facetContainer.querySelectorAll('input[type=checkbox]')) {
item.checked = false;
}
document.getElementById('searchform').dispatchEvent(new Event('submit', {bubbles:true, cancelable:true}));
});
2026-02-28 17:20:00 -05:00
</script>
2026-02-23 08:33:11 -05:00
}
2026-02-26 15:51:00 -05:00
2026-03-01 12:57:06 -05:00
{pokemon.length === 0 && (
<div id="notfound" hx-swap-oob="true">
Pokemon not found
</div>
)}
{pokemon.map((card:any) => (
<div class="col">
<div class="inventory-button position-relative float-end shadow-filter text-center d-none">
<div class="inventory-label pt-2">+/-</div>
</div>
2026-02-25 14:12:11 -05:00
<div hx-get={`/partials/card-modal?cardId=${card.cardId}`} hx-target="#cardModal" hx-trigger="click" data-bs-toggle="modal" data-bs-target="#cardModal" onclick="const cardTitle = this.querySelector('#cardImage').getAttribute('alt'); dataLayer.push({'event': 'virtualPageview', 'pageUrl': this.getAttribute('hx-get'), 'pageTitle': cardTitle, 'previousUrl': '/pokemon'});">
<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 class="row row-cols-5 gx-1 price-row mb-2">
{conditionOrder.map((condition) => (
<div class="col price-label ps-1">
{ conditionShort(condition) }
<br />{formatPrice(condition, card.skus)}
</div>
2026-02-17 13:07:29 -05:00
))}
</div>
<div class="h5 my-0">{card.productName}</div>
<div class="d-flex flex-row lh-1 mt-1">
2026-03-01 22:15:22 -05:00
<div class="text-secondary flex-grow-1">{card.setName}</div>
<div class="text-secondary">{card.number}</div>
<span class="ps-2 small-icon"><RarityIcon rarity={card.rarityName} /></span>
</div>
2026-02-24 12:36:19 -05:00
<div>{card.variant}<span class="d-none">{card.productId}</span></div>
2026-02-17 13:07:29 -05:00
</div>
2026-02-25 14:12:11 -05:00
2026-02-22 11:00:30 -05:00
))}
2026-02-23 08:33:11 -05:00
{start + 20 < totalHits &&
<div hx-post="/partials/cards" hx-trigger="revealed" hx-include="#searchform" hx-target="#cardGrid" hx-swap="beforeend" hx-on--after-request="afterUpdate(event)">
Loading...
</div>
2026-02-25 14:12:11 -05:00
}