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

249 lines
11 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import { client } from '../../db/typesense';
import RarityIcon from '../../components/RarityIcon.astro';
export const prerender = false;
import * as util from 'util';
// all the facet fields we want to use for filtering
const facetFields:any = {
"productLineName": "Product Line",
"setName": "Set",
"variant": "Variant",
"rarityName": "Rarity",
"cardType": "Card Type",
"energyType": "Energy Type"
}
// get the query from post request using form data
const formData = await Astro.request.formData();
const query = formData.get('q')?.toString() || '';
const start = Number(formData.get('start')?.toString() || '0');
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(' && ');
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',
filter_by: `sealed:false${filterBy ? ` && ${filterBy}` : ''}`,
per_page: 20,
facet_by: '',
max_facet_values: 0,
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
if (start === 0) {
for (const facet of Object.keys(facetFields)) {
searchArray.push({
collection: 'cards',
filter_by: facetFilter(facet),
per_page: 0,
facet_by: facet,
max_facet_values: 500,
page: 1,
sort_by: '',
include_fields: '',
});
}
}
const searchRequests = { searches: searchArray };
const commonSearchParams = {
q: query,
// query_by: 'productLineName,productName,setName,number,rarityName,Artist',
query_by: 'content'
};
// 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;
//console.log(util.inspect(cardResults.hits.map((c:any) => { return { productLineName:c.document.productLineName, productName:c.document.productName, setName:c.document.setName, number:c.document.number, rarityName:c.document.rarityName, Artist:c.document.Artist, text_match:c.text_match, text_match_info:c.text_match_info }; }), { showHidden: true, depth: null }));
//console.log(cardResults);
const pokemon = cardResults.hits?.map((hit: any) => hit.document) ?? [];
const totalHits = cardResults?.found;
// 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) => {
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;
if (price > 99.99) return `$${Math.round(price)}`;
return `$${price.toFixed(2)}`;
};
const conditionOrder = ["Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged"];
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('');
}
const facetNames = (name:string) => {
return facetFields[name] || name;
}
const facets = searchResults.results.slice(1).map((result: any) => {
return result.facet_counts[0];
});
---
{(start === 0) &&
<div id="facetContainer" hx-swap-oob="true">
<div class="bg-dark sticky-top p-2 d-flex justify-content-end align-items-center">
<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>
{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>
{(facet.counts.length > 20) &&
<input class="facet-filter form-control col-auto me-3" type="text" id={`filter_${facet.field_name}`} placeholder="Search..." />
}
<div class="facet-list col-12 mt-2">
{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">
<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>
</label>
</div>
))}
</div>
</div>
))}
</div>
<div id="activeFilters" class="mb-2 d-flex align-items-center justify-content-end small" hx-swap-oob="true">
{(Object.entries(filters).length > 0) &&
<span class="me-1">Filtered by:</span>
<ul class="list-group list-group-horizontal">
{Object.entries(filters).map(([filter, values]) => (
values.map((value) => (
<li data-facet={filter} data-value={value} class="list-group-item remove-filter">{value}</li>
))
))}
</ul>
<span class="ms-2"><button type="button" class="btn-close" aria-label="Clear all filters" id="clear-all-filters"></button></span>
}
</div>
<script define:vars={{ totalHits, filters, facets }} is:inline>
// Filter the facet values to make things like Set easier to find
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();
for (const item of facetitems) {
const text = item.getAttribute('data-facet-value');
if (text.includes(match)) item.style.display = ''; // Show
else item.style.display = 'none'; // Hide
}
});
}
// Clear all facets and resubmit the form with only the text box
const clearAllFilters = (e) => {
const facetContainer = document.getElementById('facetContainer');
for (const item of facetContainer.querySelectorAll('input[type=checkbox]')) {
item.checked = false;
}
document.getElementById('searchform').dispatchEvent(new Event('submit', {bubbles:true, cancelable:true}));
}
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}));
});
}
</script>
}
{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>
<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>
))}
</div>
<div class="h5 my-0">{card.productName}</div>
<div class="d-flex flex-row lh-1 mt-1 justify-content-between">
<div class="text-secondary flex-grow-1 d-none d-md-flex">{card.setName}</div>
<div class="text-secondary">{card.number}</div>
<span class="ps-2 small-icon"><RarityIcon rarity={card.rarityName} /></span>
</div>
<div>{card.variant}</div><span class="d-none">{card.productId}</span>
</div>
))}
{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>
}