Merge branch 'master' into feat/inventory

This commit is contained in:
Zach Harding
2026-03-25 08:43:18 -04:00
7 changed files with 130 additions and 6 deletions

3
.gitignore vendored
View File

@@ -26,6 +26,9 @@ pnpm-debug.log*
# imges from tcgplayer
public/cards/*
# static assets
/static/
# anything test
test.*

View File

@@ -280,6 +280,6 @@ else {
await helper.UpdateVariants(db);
// index the card updates
helper.upsertCardCollection(db);
await helper.upsertCardCollection(db);
await ClosePool();

95
src/pages/api/upload.ts Normal file
View File

@@ -0,0 +1,95 @@
// src/pages/api/upload.ts
import type { APIRoute } from 'astro';
import { parse, stringify, transform } from 'csv';
import { Readable } from 'stream';
import { client } from '../../db/typesense';
import chalk from 'chalk';
import { db, ClosePool } from '../../db/index';
// Define the transformation logic
const transformer = transform({ parallel: 1 }, async function(this: any, row: any, callback: any) {
try {
// Specific query bsaed on tcgcollector CSV
const query = String(Object.values(row)[1]);
const setname = String(Object.values(row)[4]).replace(/Wizards of the coast promos/ig,'WoTC Promo');
const cardNumber = String(Object.values(row)[7]);
console.log(`${query} ${cardNumber} : ${setname}`);
// Use Typesense to find the card because we can easily use the combined fields
let cards = await client.collections('cards').documents().search({ q: query, query_by: 'productName', filter_by: `setName:\`${setname}\` && number:${cardNumber}` });
if (cards.hits?.length === 0) {
// Try without card number
cards = await client.collections('cards').documents().search({ q: query, query_by: 'productName', filter_by: `setName:\`${setname}\`` });
}
if (cards.hits?.length === 0) {
// Try without set name
cards = await client.collections('cards').documents().search({ q: query, query_by: 'productName', filter_by: `number:${cardNumber}` });
}
if (cards.hits?.length === 0) {
// I give up, just output the values from the csv
console.log(chalk.red(' - not found'));
const newRow = { ...row };
newRow.Variant = '';
newRow.marketPrice = '';
this.push(newRow);
}
else {
for (const card of cards.hits?.map((hit: any) => hit.document) ?? []) {
console.log(chalk.blue(` - ${card.cardId} : ${card.productName} : ${card.number}`), chalk.yellow(`${card.setName}`), chalk.green(`${card.variant}`));
const variant = await db.query.cards.findFirst({
with: { prices: true, tcgdata: true },
where: { cardId: card.cardId }
});
const newRow = { ...row };
newRow.Variant = variant?.variant;
newRow.marketPrice = variant?.prices.find(p => p.condition === 'Near Mint')?.marketPrice;
this.push(newRow);
}
}
callback();
} catch (error) {
callback(error);
}
});
export const POST: APIRoute = async ({ request }) => {
try {
const formData = await request.formData();
const file = formData.get('file') as File;
const inputStream = Readable.from(file.stream());
if (!file) {
return new Response('No file uploaded', { status: 400 });
}
// Pipe the streams: Read -> Parse -> Transform -> Stringify -> Write
const outputStream = inputStream
.on('error', (error) => console.error('Input stream error:', error))
.pipe(parse({ columns: true, trim: true }))
.on('error', (error) => console.error('Parse error:', error))
.pipe(transformer)
.on('error', (error) => console.error('Transform error:', error))
.pipe(stringify({ header: true }))
.on('error', (error) => console.error('Stringify error:', error));
// outputStream.on('finish', () => {
// ClosePool();
// }).on('error', (error) => {
// ClosePool();
// });
return new Response(outputStream as any, {
headers: {
'Content-Type': 'text/csv',
'Content-Disposition': 'attachment; filename=transformed.csv',
},
});
} catch (error) {
console.error('Error processing CSV stream:', error);
return new Response('Internal Server Error', { status: 500 });
}
};

26
src/pages/myprices.astro Normal file
View File

@@ -0,0 +1,26 @@
---
import Layout from '../layouts/Main.astro';
import NavItems from '../components/NavItems.astro';
import NavBar from '../components/NavBar.astro';
import Footer from '../components/Footer.astro';
---
<Layout title="Rigid's App Thing">
<NavBar slot="navbar">
<NavItems slot="navItems" />
</NavBar>
<div class="row mb-4" slot="page">
<div class="col-12">
<h1>Rigid's App Thing</h1>
<p class="text-secondary">(working title)</p>
</div>
<div class="col-12">
<!-- src/components/FileUploader.astro -->
<form action="/api/upload" method="post" enctype="multipart/form-data">
<input type="file" name="file" accept=".csv" required />
<button type="submit">Upload CSV</button>
</form>
</div>
</div>
<Footer slot="footer" />
</Layout>

View File

@@ -46,7 +46,7 @@ const calculatedAt = (() => {
const dates = card.prices
.map(p => p.calculatedAt)
.filter(d => d)
.map(d => new Date(d));
.map(d => new Date(d!));
if (!dates.length) return null;
return new Date(Math.max(...dates.map(d => d.getTime())));
})();
@@ -201,11 +201,11 @@ const altSearchUrl = (card: any) => {
data-name={card?.productName}
>
<img
src={`/cards/${card?.productId}.jpg`}
src={`/static/cards/${card?.productId}.jpg`}
class="card-image w-100 img-fluid rounded-4"
alt={card?.productName}
crossorigin="anonymous"
onerror="this.onerror=null; this.src='/cards/default.jpg'; this.closest('.image-grow, .card-image-wrap')?.setAttribute('data-default','true')"
onerror="this.onerror=null; this.src='/static/cards/default.jpg'; this.closest('.image-grow, .card-image-wrap')?.setAttribute('data-default','true')"
onclick="copyImage(this); dataLayer.push({'event': 'copiedImage'});"
/>
</div>

View File

@@ -287,7 +287,7 @@ const facets = searchResults.results.slice(1).map((result: any) => {
<b>+/</b>
</button>
<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'});">
<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={`/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='/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>
<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>
<div class="holo-shine"></div>
<div class="holo-glare"></div>
</div>

View File

@@ -1,5 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"include": [".astro/types.d.ts", "src/**/*"],
"exclude": ["dist"]
}