@@ -48,12 +48,9 @@ import BackToTop from "./BackToTop.astro"
(function () {
// ── Sort dropdown ─────────────────────────────────────────────────────────
- // Plain JS toggle — no dependency on Bootstrap's Dropdown JS initialising.
- // Uses event delegation so it works after OOB swaps repopulate #sortBy.
document.addEventListener('click', (e) => {
const sortBy = document.getElementById('sortBy');
- // Toggle the menu when the button is clicked
const btn = e.target.closest('#sortBy [data-bs-toggle="dropdown"]');
if (btn) {
e.preventDefault();
@@ -64,7 +61,6 @@ import BackToTop from "./BackToTop.astro"
return;
}
- // Handle sort option selection
const opt = e.target.closest('#sortBy .sort-option');
if (opt) {
e.preventDefault();
@@ -87,7 +83,6 @@ import BackToTop from "./BackToTop.astro"
return;
}
- // Click outside — close any open sort menu
const menu = document.querySelector('#sortBy .dropdown-menu.show');
if (menu) {
menu.classList.remove('show');
@@ -96,6 +91,25 @@ import BackToTop from "./BackToTop.astro"
}
});
+ // ── Language toggle ───────────────────────────────────────────────────────
+ // Buttons live inside #sortBy which is OOB-swapped from the partial, so the
+ // listener is registered here on document (persistent shell) instead.
+ document.addEventListener('click', (e) => {
+ const btn = e.target.closest('.language-btn');
+ if (!btn) return;
+ e.preventDefault();
+
+ const input = document.getElementById('languageInput');
+ if (input) input.value = btn.dataset.lang;
+
+ const start = document.getElementById('start');
+ if (start) start.value = '0';
+
+ document.getElementById('searchform').dispatchEvent(
+ new Event('submit', { bubbles: true, cancelable: true })
+ );
+ });
+
// ── Global helpers ────────────────────────────────────────────────────────
window.copyImage = async function(img) {
try {
@@ -201,7 +215,6 @@ import BackToTop from "./BackToTop.astro"
nextBtn.classList.toggle('d-none', next === null);
}
- // ── Trigger infinite scroll sentinel ─────────────────────────────────────
function tryTriggerSentinel() {
const sentinel = cardGrid.querySelector('[hx-trigger="revealed"]');
if (!sentinel) return;
@@ -212,7 +225,6 @@ import BackToTop from "./BackToTop.astro"
}
}
- // ── Fire card-modal:swapped so the partial's script can init the chart ────
function initChartAfterSwap(modal) {
const canvas = modal.querySelector('#priceHistoryChart');
if (!canvas) return;
@@ -269,11 +281,9 @@ import BackToTop from "./BackToTop.astro"
if (next) loadCard(next, 'next');
}
- // ── Nav button clicks ─────────────────────────────────────────────────────
document.getElementById('modalPrevBtn').addEventListener('click', navigatePrev);
document.getElementById('modalNextBtn').addEventListener('click', navigateNext);
- // ── Keyboard ──────────────────────────────────────────────────────────────
document.addEventListener('keydown', (e) => {
const modal = document.getElementById('cardModal');
if (!modal.classList.contains('show')) return;
@@ -281,7 +291,6 @@ import BackToTop from "./BackToTop.astro"
if (e.key === 'ArrowRight') { e.preventDefault(); navigateNext(); }
});
- // ── Touch / swipe ─────────────────────────────────────────────────────────
let touchStartX = 0;
let touchStartY = 0;
const SWIPE_THRESHOLD = 50;
@@ -299,7 +308,6 @@ import BackToTop from "./BackToTop.astro"
else navigatePrev();
}, { passive: true });
- // ── HTMX card-modal opens ─────────────────────────────────────────────────
document.body.addEventListener('htmx:beforeRequest', async (e) => {
if (e.detail.elt.getAttribute('hx-target') !== '#cardModal') return;
@@ -363,7 +371,6 @@ import BackToTop from "./BackToTop.astro"
}
});
- // ── Bootstrap modal events ────────────────────────────────────────────────
const cardModal = document.getElementById('cardModal');
cardModal.addEventListener('shown.bs.modal', () => {
updateNavButtons(cardModal);
diff --git a/src/components/Search.astro b/src/components/Search.astro
index 150f03e..457e8dd 100644
--- a/src/components/Search.astro
+++ b/src/components/Search.astro
@@ -26,15 +26,21 @@ import { Show } from '@clerk/astro/components'
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/src/pages/partials/cards.astro b/src/pages/partials/cards.astro
index 665da0c..446ca68 100644
--- a/src/pages/partials/cards.astro
+++ b/src/pages/partials/cards.astro
@@ -18,11 +18,6 @@ const facetFields:any = {
}
// ── Allowed sort values ───────────────────────────────────────────────────
-// Maps the client-supplied key to the actual Typesense sort_by string.
-// Never pass raw user input directly to sort_by.
-// Note: price sorting uses nmMarketPrice — a field you need to denormalize
-// onto your card document in your Typesense indexing step (NM market price
-// as an integer in cents, e.g. nmMarketPrice: 499 = $4.99).
const sortMap: Record
= {
'releaseDate:desc,number:asc': '_text_match:asc,releaseDate:desc,number:asc',
'releaseDate:asc,number:asc': '_text_match:asc,releaseDate:asc,number:asc',
@@ -40,8 +35,16 @@ const start = Number(formData.get('start')?.toString() || '0');
const sortKey = formData.get('sort')?.toString() || '';
const resolvedSort = sortMap[sortKey] ?? DEFAULT_SORT;
+// ── 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`"
+ : '';
+
const filters = Array.from(formData.entries())
- .filter(([key, value]) => key !== 'q' && key !== 'start' && key !== 'sort')
+ .filter(([key]) => key !== 'q' && key !== 'start' && key !== 'sort' && key !== 'language')
.reduce((acc, [key, value]) => {
if (!acc[key]) {
acc[key] = [];
@@ -63,14 +66,15 @@ const facetFilter = (facet:string) => {
.filter(([field]) => field !== facet)
.map(([field, values]) => `${field}:=[${values.map(v => '`'+v+'`').join(',')}]`)
.join(' && ');
- return `sealed:false${otherFilters ? ` && ${otherFilters}` : ''}`;
+ // Language filter is always included so facet counts stay accurate
+ return `sealed:false${languageFilter}${otherFilters ? ` && ${otherFilters}` : ''}`;
};
// primary search values (for cards)
let searchArray = [{
collection: 'cards',
- filter_by: `sealed:false${filterBy ? ` && ${filterBy}` : ''}`,
+ filter_by: `sealed:false${languageFilter}${filterBy ? ` && ${filterBy}` : ''}`,
per_page: 20,
facet_by: '',
max_facet_values: 0,
@@ -135,8 +139,8 @@ const facetNames = (name:string) => {
}
const facets = searchResults.results.slice(1).map((result: any) => {
- const facet = result.facet_counts[0];
- if (!facet) return facet;
+ const facet = result.facet_counts?.[0];
+ if (!facet) return null;
// Sort: checked items first, then alphabetically
facet.counts = facet.counts.sort((a: any, b: any) => {
@@ -148,7 +152,7 @@ const facets = searchResults.results.slice(1).map((result: any) => {
});
return facet;
-});
+}).filter(Boolean);
---
@@ -178,19 +182,24 @@ const facets = searchResults.results.slice(1).map((result: any) => {