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-03-09 12:00:29 -04:00
|
|
|
|
import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
|
2026-02-22 11:00:30 -05:00
|
|
|
|
export const prerender = false;
|
|
|
|
|
|
|
2026-03-05 16:08:58 -05:00
|
|
|
|
import * as util from 'util';
|
|
|
|
|
|
|
|
|
|
|
|
|
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-03-17 17:27:39 -04:00
|
|
|
|
//"productLineName": "Product Line",
|
2026-02-27 15:27:10 -05:00
|
|
|
|
"setName": "Set",
|
|
|
|
|
|
"variant": "Variant",
|
|
|
|
|
|
"rarityName": "Rarity",
|
|
|
|
|
|
"cardType": "Card Type",
|
|
|
|
|
|
"energyType": "Energy Type"
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-16 14:39:55 -04:00
|
|
|
|
// ── Allowed sort values ───────────────────────────────────────────────────
|
|
|
|
|
|
const sortMap: Record<string, string> = {
|
|
|
|
|
|
'releaseDate:desc,number:asc': '_text_match:asc,releaseDate:desc,number:asc',
|
|
|
|
|
|
'releaseDate:asc,number:asc': '_text_match:asc,releaseDate:asc,number:asc',
|
|
|
|
|
|
'marketPrice:desc': 'marketPrice:desc,releaseDate:desc,number:asc',
|
|
|
|
|
|
'marketPrice:asc': 'marketPrice:asc,releaseDate:desc,number:asc',
|
|
|
|
|
|
'number:asc': '_text_match:asc,number:asc',
|
|
|
|
|
|
'number:desc': '_text_match:asc,number:desc',
|
|
|
|
|
|
};
|
|
|
|
|
|
const DEFAULT_SORT = '_text_match:asc,releaseDate:desc,number:asc';
|
2026-02-27 15:27:10 -05:00
|
|
|
|
|
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-03-16 14:39:55 -04:00
|
|
|
|
const sortKey = formData.get('sort')?.toString() || '';
|
|
|
|
|
|
const resolvedSort = sortMap[sortKey] ?? DEFAULT_SORT;
|
|
|
|
|
|
|
2026-03-17 11:27:16 -04:00
|
|
|
|
// ── Language filter ───────────────────────────────────────────────────────
|
|
|
|
|
|
// Expects a `language` field on your card documents in Typesense.
|
|
|
|
|
|
// Valid values: 'en', 'jp' — anything else (or 'all') means no filter.
|
|
|
|
|
|
const language = formData.get('language')?.toString() || 'all';
|
|
|
|
|
|
const languageFilter = language === 'en' ? " && productLineName:=`Pokemon`"
|
|
|
|
|
|
: language === 'jp' ? " && productLineName:=`Pokemon Japan`"
|
|
|
|
|
|
: '';
|
|
|
|
|
|
|
2026-03-17 17:27:39 -04:00
|
|
|
|
// ── Query alias expansion ─────────────────────────────────────────────────
|
|
|
|
|
|
// Intercepts known shorthand queries that can't be handled by Typesense
|
|
|
|
|
|
// synonyms alone (e.g. terms that need to match across multiple set names)
|
|
|
|
|
|
// and rewrites them into a direct filter, clearing the query so it doesn't
|
|
|
|
|
|
// also try to text-match against card names.
|
|
|
|
|
|
const EREADER_SETS = ['Expedition Base Set', 'Aquapolis', 'Skyridge', 'Battle-e'];
|
|
|
|
|
|
const EREADER_RE = /^(e-?reader|e reader)$/i;
|
|
|
|
|
|
|
|
|
|
|
|
let resolvedQuery = query;
|
|
|
|
|
|
let queryFilter = '';
|
|
|
|
|
|
|
|
|
|
|
|
if (EREADER_RE.test(query.trim())) {
|
|
|
|
|
|
resolvedQuery = '';
|
|
|
|
|
|
queryFilter = `setName:=[${EREADER_SETS.map(s => '`' + s + '`').join(',')}]`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 15:51:00 -05:00
|
|
|
|
const filters = Array.from(formData.entries())
|
2026-03-17 11:27:16 -04:00
|
|
|
|
.filter(([key]) => key !== 'q' && key !== 'start' && key !== 'sort' && key !== 'language')
|
2026-02-26 15:51:00 -05:00
|
|
|
|
.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]) => {
|
2026-03-11 20:33:43 -04:00
|
|
|
|
return `${field}:=[${values.map(v => '`'+v+'`').join(',')}]`;
|
2026-02-26 15:51:00 -05:00
|
|
|
|
}).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)
|
2026-03-11 20:33:43 -04:00
|
|
|
|
.map(([field, values]) => `${field}:=[${values.map(v => '`'+v+'`').join(',')}]`)
|
2026-02-27 15:27:10 -05:00
|
|
|
|
.join(' && ');
|
2026-03-17 11:27:16 -04:00
|
|
|
|
// Language filter is always included so facet counts stay accurate
|
2026-03-17 17:27:39 -04:00
|
|
|
|
return `sealed:false${languageFilter}${queryFilter ? ` && ${queryFilter}` : ''}${otherFilters ? ` && ${otherFilters}` : ''}`;
|
2026-02-27 15:27:10 -05:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// primary search values (for cards)
|
|
|
|
|
|
let searchArray = [{
|
|
|
|
|
|
collection: 'cards',
|
2026-03-17 17:27:39 -04:00
|
|
|
|
filter_by: `sealed:false${languageFilter}${queryFilter ? ` && ${queryFilter}` : ''}${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,
|
2026-03-16 14:39:55 -04:00
|
|
|
|
sort_by: resolvedSort,
|
2026-02-28 10:19:40 -05:00
|
|
|
|
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,
|
2026-02-28 10:19:40 -05:00
|
|
|
|
sort_by: '',
|
|
|
|
|
|
include_fields: '',
|
2026-02-27 15:27:10 -05:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const searchRequests = { searches: searchArray };
|
|
|
|
|
|
const commonSearchParams = {
|
2026-03-17 17:27:39 -04:00
|
|
|
|
q: resolvedQuery,
|
|
|
|
|
|
query_by: 'content,setName,productLineName,rarityName,energyType,cardType'
|
2026-02-27 15:27:10 -05:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
2026-02-28 10:19:40 -05:00
|
|
|
|
const pokemon = cardResults.hits?.map((hit: any) => hit.document) ?? [];
|
2026-02-27 15:27:10 -05:00
|
|
|
|
const totalHits = cardResults?.found;
|
2026-02-28 10:19:40 -05:00
|
|
|
|
|
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 "–"
|
2026-02-28 10:19:40 -05:00
|
|
|
|
const formatPrice = (condition:string, skus: any) => {
|
2026-03-01 12:57:06 -05:00
|
|
|
|
const sku:any = skus.find((price:any) => price.condition === condition);
|
2026-02-28 10:19:40 -05:00
|
|
|
|
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) => {
|
2026-03-17 11:27:16 -04:00
|
|
|
|
const facet = result.facet_counts?.[0];
|
|
|
|
|
|
if (!facet) return null;
|
2026-03-09 14:25:18 -04:00
|
|
|
|
|
|
|
|
|
|
// Sort: checked items first, then alphabetically
|
|
|
|
|
|
facet.counts = facet.counts.sort((a: any, b: any) => {
|
|
|
|
|
|
const aChecked = filters[facet.field_name]?.includes(a.value) ?? false;
|
|
|
|
|
|
const bChecked = filters[facet.field_name]?.includes(b.value) ?? false;
|
|
|
|
|
|
if (aChecked && !bChecked) return -1;
|
|
|
|
|
|
if (!aChecked && bChecked) return 1;
|
|
|
|
|
|
return a.value.localeCompare(b.value);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return facet;
|
2026-03-17 11:27:16 -04:00
|
|
|
|
}).filter(Boolean);
|
2026-02-26 15:51:00 -05:00
|
|
|
|
|
2026-02-17 13:07:29 -05:00
|
|
|
|
---
|
2026-03-11 15:21:43 -04: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">
|
2026-02-28 20:47:32 -05:00
|
|
|
|
<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>
|
2026-03-01 20:04:35 -05:00
|
|
|
|
<button type="submit" form="searchform" data-bs-dismiss="offcanvas" class="btn btn-success">Apply Filters</button>
|
2026-02-28 20:47:32 -05:00
|
|
|
|
</div>
|
2026-02-26 15:51:00 -05:00
|
|
|
|
{facets.map((facet) => (
|
2026-02-28 20:47:32 -05:00
|
|
|
|
<div class="mt-2 mb-4 facet-group row align-items-center justify-content-between">
|
2026-03-01 20:04:35 -05:00
|
|
|
|
<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) &&
|
2026-02-28 20:47:32 -05:00
|
|
|
|
<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
|
|
|
|
}
|
2026-03-02 14:09:59 -05:00
|
|
|
|
<div class="facet-list col-12 mt-2">
|
2026-03-01 12:57:06 -05:00
|
|
|
|
{facet.counts.map((count:any) => (
|
2026-03-01 20:04:35 -05:00
|
|
|
|
<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" />
|
2026-03-01 20:04:35 -05:00
|
|
|
|
{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-03-17 17:27:39 -04:00
|
|
|
|
<div id="sortBy" class="d-flex flex-fill align-items-center me-auto gap-2" hx-swap-oob="true">
|
2026-03-16 14:39:55 -04:00
|
|
|
|
<div class="dropdown">
|
2026-03-17 10:35:02 -04:00
|
|
|
|
<button class="btn btn-sm btn-dark dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Sort by</button>
|
2026-03-16 14:39:55 -04:00
|
|
|
|
<ul class="dropdown-menu dropdown-menu-dark">
|
2026-03-17 17:27:39 -04:00
|
|
|
|
<li><a class="dropdown-item sort-option" href="#" data-sort="releaseDate:desc,number:asc" data-label="Set: Newest to Oldest">Set: Newest to Oldest</a></li>
|
|
|
|
|
|
<li><a class="dropdown-item sort-option" href="#" data-sort="releaseDate:asc,number:asc" data-label="Set: Oldest to Newest">Set: Oldest to Newest</a></li>
|
|
|
|
|
|
<li><a class="dropdown-item sort-option" href="#" data-sort="marketPrice:desc" data-label="Price: High to Low">Price: High to Low</a></li>
|
|
|
|
|
|
<li><a class="dropdown-item sort-option" href="#" data-sort="marketPrice:asc" data-label="Price: Low to High">Price: Low to High</a></li>
|
|
|
|
|
|
<li><a class="dropdown-item sort-option" href="#" data-sort="number:asc" data-label="Card Number: Ascending">Card Number: Ascending</a></li>
|
|
|
|
|
|
<li><a class="dropdown-item sort-option" href="#" data-sort="number:desc" data-label="Card Number: Descending">Card Number: Descending</a></li>
|
2026-03-16 14:39:55 -04:00
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2026-03-17 17:27:39 -04:00
|
|
|
|
<span id="sortLabel" class="ms-1 text-secondary small">{sortKey ? ({"releaseDate:desc,number:asc":"Set: Newest to Oldest","releaseDate:asc,number:asc":"Set: Oldest to Newest","marketPrice:desc":"Price: High to Low","marketPrice:asc":"Price: Low to High","number:asc":"Card Number: Ascending","number:desc":"Card Number: Descending"}[sortKey] ?? '') : ''}</span>
|
|
|
|
|
|
<div class="btn-group btn-group-sm ms-2 flex-shrink-0" role="group" aria-label="Language filter">
|
|
|
|
|
|
<button type="button" class={`btn btn-dark language-btn${language === 'all' ? ' active' : ''}`} data-lang="all">All</button>
|
|
|
|
|
|
<button type="button" class={`btn btn-dark language-btn${language === 'en' ? ' active' : ''}`} data-lang="en">EN</button>
|
|
|
|
|
|
<button type="button" class={`btn btn-dark language-btn${language === 'jp' ? ' active' : ''}`} data-lang="jp">JP</button>
|
|
|
|
|
|
</div>
|
2026-03-16 14:39:55 -04:00
|
|
|
|
</div>
|
2026-03-17 10:35:02 -04:00
|
|
|
|
<div id="totalResults" class="d-flex text-secondary small d-none align-items-center" hx-swap-oob="true">
|
2026-03-11 15:21:43 -04:00
|
|
|
|
{totalHits} {totalHits === 1 ? ' result' : ' results'}
|
|
|
|
|
|
</div>
|
2026-03-17 10:35:02 -04:00
|
|
|
|
<div id="activeFilters" class="d-flex small ms-auto align-items-center" hx-swap-oob="true">
|
2026-03-02 16:13:24 -05:00
|
|
|
|
{(Object.entries(filters).length > 0) &&
|
2026-03-16 14:39:55 -04:00
|
|
|
|
<span class="me-1 small">Filtered by:</span>
|
2026-03-02 16:13:24 -05:00
|
|
|
|
<ul class="list-group list-group-horizontal">
|
|
|
|
|
|
{Object.entries(filters).map(([filter, values]) => (
|
|
|
|
|
|
values.map((value) => (
|
2026-03-17 10:35:02 -04:00
|
|
|
|
<li data-facet={filter} data-value={value} class="list-group-item small p-2 remove-filter">{value}</li>
|
2026-03-02 16:13:24 -05:00
|
|
|
|
))
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<span class="ms-2"><button type="button" class="btn-close" aria-label="Clear all filters" id="clear-all-filters"></button></span>
|
|
|
|
|
|
}
|
|
|
|
|
|
</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
|
2026-03-02 16:13:24 -05:00
|
|
|
|
const clearAllFilters = (e) => {
|
|
|
|
|
|
const facetContainer = document.getElementById('facetContainer');
|
2026-03-02 11:48:10 -05:00
|
|
|
|
for (const item of facetContainer.querySelectorAll('input[type=checkbox]')) {
|
|
|
|
|
|
item.checked = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
document.getElementById('searchform').dispatchEvent(new Event('submit', {bubbles:true, cancelable:true}));
|
2026-03-02 16:13:24 -05:00
|
|
|
|
}
|
|
|
|
|
|
document.getElementById('clear-filters').addEventListener('click', (e) => clearAllFilters(e));
|
|
|
|
|
|
document.getElementById('clear-all-filters').addEventListener('click', (e) => clearAllFilters(e));
|
|
|
|
|
|
|
|
|
|
|
|
// Remove single facet value
|
|
|
|
|
|
for (const li of document.querySelectorAll('.remove-filter')) {
|
|
|
|
|
|
li.addEventListener('click', (e) => {
|
|
|
|
|
|
const filter = li.getAttribute('data-facet');
|
|
|
|
|
|
const value = li.getAttribute('data-value');
|
|
|
|
|
|
document.querySelector(`.form-check-input[name="${filter}"][value="${value}"]`).checked = false;
|
|
|
|
|
|
document.getElementById('searchform').dispatchEvent(new Event('submit', {bubbles:true, cancelable:true}));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-03-05 22:59:16 -05:00
|
|
|
|
|
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) => (
|
2026-02-21 16:26:34 -05:00
|
|
|
|
<div class="col">
|
2026-02-23 17:04:45 -05:00
|
|
|
|
<div class="inventory-button position-relative float-end shadow-filter text-center d-none">
|
|
|
|
|
|
<div class="inventory-label pt-2">+/-</div>
|
|
|
|
|
|
</div>
|
2026-03-09 12:00:29 -04:00
|
|
|
|
<div class="card-trigger position-relative" data-card-id={card.cardId} 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'});">
|
2026-03-25 05:34:11 -04:00
|
|
|
|
<div class="image-grow rounded-4 card-image" data-energy={card.energyType} data-rarity={card.rarityName} data-variant={card.variant} data-name={card.productName}><img src={`/static/cards/${card.productId}.jpg`} alt={card.productName} id="cardImage" loading="lazy" decoding="async" class="img-fluid rounded-4 mb-2 w-100" onerror="this.onerror=null; this.src='/static/cards/default.jpg'; this.closest('.image-grow')?.setAttribute('data-default','true')"/><span class="position-absolute top-50 start-0 d-inline medium-icon"><FirstEditionIcon edition={card?.variant} /></span>
|
2026-03-18 20:36:33 -04:00
|
|
|
|
<div class="holo-shine"></div>
|
|
|
|
|
|
<div class="holo-glare"></div>
|
|
|
|
|
|
</div>
|
2026-02-21 16:26:34 -05:00
|
|
|
|
</div>
|
2026-02-19 07:55:18 -05:00
|
|
|
|
<div class="row row-cols-5 gx-1 price-row mb-2">
|
2026-02-28 10:19:40 -05:00
|
|
|
|
{conditionOrder.map((condition) => (
|
2026-03-05 12:19:46 -05:00
|
|
|
|
<div class="col price-label ps-1">
|
2026-02-28 10:19:40 -05:00
|
|
|
|
{ conditionShort(condition) }
|
|
|
|
|
|
<br />{formatPrice(condition, card.skus)}
|
|
|
|
|
|
</div>
|
2026-02-17 13:07:29 -05:00
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
2026-02-19 07:55:18 -05:00
|
|
|
|
<div class="h5 my-0">{card.productName}</div>
|
2026-03-03 13:36:11 -05:00
|
|
|
|
<div class="d-flex flex-row lh-1 mt-1 justify-content-between">
|
2026-03-09 14:25:18 -04:00
|
|
|
|
<div class="text-secondary flex-grow-1 d-none d-lg-flex">{card.setName}</div>
|
|
|
|
|
|
<div class="text-body-tertiary">{card.number}</div>
|
2026-02-20 09:11:36 -05:00
|
|
|
|
<span class="ps-2 small-icon"><RarityIcon rarity={card.rarityName} /></span>
|
2026-02-19 07:55:18 -05:00
|
|
|
|
</div>
|
2026-03-09 14:25:18 -04:00
|
|
|
|
<div class="text-body-tertiary">{card.variant}</div><span class="d-none">{card.productId}</span>
|
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
|
|
|
|
}
|