adsense verification, remove latest sales table, add new search mechanic (weight, synonyms), fix low volatility (NaN%)
This commit is contained in:
@@ -54,6 +54,7 @@ export const createCardCollection = async () => {
|
|||||||
{ name: 'productLineName', type: 'string', facet: true },
|
{ name: 'productLineName', type: 'string', facet: true },
|
||||||
{ name: 'rarityName', type: 'string', facet: true },
|
{ name: 'rarityName', type: 'string', facet: true },
|
||||||
{ name: 'setName', type: 'string', facet: true },
|
{ name: 'setName', type: 'string', facet: true },
|
||||||
|
{ name: 'setCode', type: 'string' },
|
||||||
{ name: 'cardType', type: 'string', facet: true },
|
{ name: 'cardType', type: 'string', facet: true },
|
||||||
{ name: 'energyType', type: 'string', facet: true },
|
{ name: 'energyType', type: 'string', facet: true },
|
||||||
{ name: 'number', type: 'string', sort: true },
|
{ name: 'number', type: 'string', sort: true },
|
||||||
@@ -113,12 +114,13 @@ export const upsertCardCollection = async (db:DBInstance) => {
|
|||||||
productLineName: card.productLineName,
|
productLineName: card.productLineName,
|
||||||
rarityName: card.rarityName,
|
rarityName: card.rarityName,
|
||||||
setName: card.set?.setName || "",
|
setName: card.set?.setName || "",
|
||||||
|
setCode: card.set?.setCode || "",
|
||||||
cardType: card.cardType || "",
|
cardType: card.cardType || "",
|
||||||
energyType: card.energyType || "",
|
energyType: card.energyType || "",
|
||||||
number: card.number,
|
number: card.number,
|
||||||
Artist: card.artist || "",
|
Artist: card.artist || "",
|
||||||
sealed: card.sealed,
|
sealed: card.sealed,
|
||||||
content: [card.productName, card.productLineName, card.set?.setName || "", card.number, card.rarityName, card.artist || ""].join(' '),
|
content: [card.productName, card.productLineName, card.set?.setName || "", card.set?.setCode || "", card.number, card.rarityName, card.artist || ""].join(' '),
|
||||||
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,
|
||||||
...(marketPrice !== null && { marketPrice }),
|
...(marketPrice !== null && { marketPrice }),
|
||||||
sku_id: card.prices.map(price => price.skuId.toString())
|
sku_id: card.prices.map(price => price.skuId.toString())
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const { title } = Astro.props;
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="htmx-config" content='{"historyCacheSize": 50}'/>
|
<meta name="htmx-config" content='{"historyCacheSize": 50}'/>
|
||||||
|
<meta name="google-adsense-account" content="ca-pub-1140571217687341">
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
|
|||||||
@@ -88,13 +88,22 @@ for (const row of historyRows) {
|
|||||||
|
|
||||||
function computeVolatility(prices: number[]): { label: string; monthlyVol: number } {
|
function computeVolatility(prices: number[]): { label: string; monthlyVol: number } {
|
||||||
if (prices.length < 2) return { label: '—', monthlyVol: 0 };
|
if (prices.length < 2) return { label: '—', monthlyVol: 0 };
|
||||||
|
|
||||||
const returns: number[] = [];
|
const returns: number[] = [];
|
||||||
for (let i = 1; i < prices.length; i++) {
|
for (let i = 1; i < prices.length; i++) {
|
||||||
returns.push(Math.log(prices[i] / prices[i - 1]));
|
const ratio = prices[i] / prices[i - 1];
|
||||||
|
if (!isFinite(ratio) || ratio <= 0) continue; // skip bad ratios
|
||||||
|
returns.push(Math.log(ratio));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (returns.length < 2) return { label: '—', monthlyVol: 0 }; // ← key fix
|
||||||
|
|
||||||
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
|
||||||
const variance = returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / (returns.length - 1);
|
const variance = returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / (returns.length - 1);
|
||||||
const monthlyVol = Math.sqrt(variance) * Math.sqrt(30);
|
const monthlyVol = Math.sqrt(variance) * Math.sqrt(30);
|
||||||
|
|
||||||
|
if (!isFinite(monthlyVol)) return { label: '—', monthlyVol: 0 }; // safety net
|
||||||
|
|
||||||
const label = monthlyVol >= 0.30 ? 'High'
|
const label = monthlyVol >= 0.30 ? 'High'
|
||||||
: monthlyVol >= 0.15 ? 'Medium'
|
: monthlyVol >= 0.15 ? 'Medium'
|
||||||
: 'Low';
|
: 'Low';
|
||||||
@@ -310,7 +319,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
|
|
||||||
<!-- Table only — chart is outside the tab panes -->
|
<!-- Table only — chart is outside the tab panes -->
|
||||||
<div class="w-100">
|
<div class="w-100">
|
||||||
<div class="alert alert-dark rounded p-2 mb-0 table-responsive">
|
<div class="alert alert-dark rounded p-2 mb-0 table-responsive d-none">
|
||||||
<h6>Latest Verified Sales</h6>
|
<h6>Latest Verified Sales</h6>
|
||||||
<table class="table table-sm mb-0">
|
<table class="table table-sm mb-0">
|
||||||
<caption class="small">Filtered to remove mismatched language variants</caption>
|
<caption class="small">Filtered to remove mismatched language variants</caption>
|
||||||
|
|||||||
@@ -48,15 +48,53 @@ const languageFilter = language === 'en' ? " && productLineName:=`Pokemon`"
|
|||||||
// synonyms alone (e.g. terms that need to match across multiple set names)
|
// 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
|
// and rewrites them into a direct filter, clearing the query so it doesn't
|
||||||
// also try to text-match against card names.
|
// 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;
|
const ALIAS_FILTERS = [
|
||||||
|
// ── Era / set groupings ───────────────────────────────────────────────
|
||||||
|
{ re: /^(e-?reader|e reader)$/i, field: 'setName',
|
||||||
|
values: ['Expedition Base Set', 'Aquapolis', 'Skyridge', 'Battle-e'] },
|
||||||
|
|
||||||
|
{ re: /^neo$/i, field: 'setName',
|
||||||
|
values: ['Neo Genesis', 'Neo Discovery', 'Neo Revelation', 'Neo Destiny'] },
|
||||||
|
|
||||||
|
{ re: /^(wotc|wizards)$/i, field: 'setName',
|
||||||
|
values: ['Base Set', 'Jungle', 'Fossil', 'Base Set 2', 'Team Rocket',
|
||||||
|
'Gym Heroes', 'Gym Challenge', 'Neo Genesis', 'Neo Discovery',
|
||||||
|
'Neo Revelation', 'Neo Destiny', 'Expedition Base Set',
|
||||||
|
'Aquapolis', 'Skyridge', 'Battle-e'] },
|
||||||
|
|
||||||
|
{ re: /^(sun\s*(&|and)\s*moon|s(&|and)m|sm)$/i, field: 'setName',
|
||||||
|
values: ['Sun & Moon', 'Guardians Rising', 'Burning Shadows', 'Crimson Invasion',
|
||||||
|
'Ultra Prism', 'Forbidden Light', 'Celestial Storm', 'Dragon Majesty',
|
||||||
|
'Lost Thunder', 'Team Up', 'Unbroken Bonds', 'Unified Minds',
|
||||||
|
'Hidden Fates', 'Cosmic Eclipse', 'Detective Pikachu'] },
|
||||||
|
|
||||||
|
{ re: /^(sword\s*(&|and)\s*shield|s(&|and)s|swsh)$/i, field: 'setName',
|
||||||
|
values: ['Sword & Shield', 'Rebel Clash', 'Darkness Ablaze', 'Vivid Voltage',
|
||||||
|
'Battle Styles', 'Chilling Reign', 'Evolving Skies', 'Fusion Strike',
|
||||||
|
'Brilliant Stars', 'Astral Radiance', 'Pokemon GO', 'Lost Origin',
|
||||||
|
'Silver Tempest', 'Crown Zenith'] },
|
||||||
|
|
||||||
|
// ── Card type shorthands ──────────────────────────────────────────────
|
||||||
|
{ re: /^trainers?$/i, field: 'cardType', values: ['Trainer'] },
|
||||||
|
{ re: /^supporters?$/i, field: 'cardType', values: ['Supporter'] },
|
||||||
|
{ re: /^stadiums?$/i, field: 'cardType', values: ['Stadium'] },
|
||||||
|
{ re: /^items?$/i, field: 'cardType', values: ['Item'] },
|
||||||
|
{ re: /^(energys?|energies)$/i, field: 'cardType', values: ['Energy'] },
|
||||||
|
|
||||||
|
// ── Rarity shorthands ─────────────────────────────────────────────────
|
||||||
|
{ re: /^promos?$/i, field: 'rarityName', values: ['Promo'] },
|
||||||
|
];
|
||||||
|
|
||||||
let resolvedQuery = query;
|
let resolvedQuery = query;
|
||||||
let queryFilter = '';
|
let queryFilter = '';
|
||||||
|
|
||||||
if (EREADER_RE.test(query.trim())) {
|
for (const alias of ALIAS_FILTERS) {
|
||||||
resolvedQuery = '';
|
if (alias.re.test(query.trim())) {
|
||||||
queryFilter = `setName:=[${EREADER_SETS.map(s => '`' + s + '`').join(',')}]`;
|
resolvedQuery = '';
|
||||||
|
queryFilter = `${alias.field}:=[${alias.values.map(s => '`' + s + '`').join(',')}]`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filters = Array.from(formData.entries())
|
const filters = Array.from(formData.entries())
|
||||||
@@ -118,7 +156,10 @@ if (start === 0) {
|
|||||||
const searchRequests = { searches: searchArray };
|
const searchRequests = { searches: searchArray };
|
||||||
const commonSearchParams = {
|
const commonSearchParams = {
|
||||||
q: resolvedQuery,
|
q: resolvedQuery,
|
||||||
query_by: 'content,setName,productLineName,rarityName,energyType,cardType'
|
query_by: 'content,setName,setCode,productName',
|
||||||
|
query_by_weights: '10,6,8,9',
|
||||||
|
num_typos: '2,1,0,1',
|
||||||
|
prefix: 'true,true,false,false',
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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
|
||||||
@@ -276,8 +317,8 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
{pokemon.length === 0 && (
|
{pokemon.length === 0 && (
|
||||||
<div id="notfound" hx-swap-oob="true">
|
<div id="notfound" class="mt-4 h6" hx-swap-oob="true">
|
||||||
Pokemon not found
|
No cards found! Please modify your search and try again.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user