Compare commits
15 Commits
5d716a4d8e
...
feat/inven
| Author | SHA1 | Date | |
|---|---|---|---|
| 29deb19b89 | |||
|
|
e7374ad182 | ||
| 37b9d5c954 | |||
| ed049da3d2 | |||
| 9a8008fc92 | |||
|
|
7b1f470ee9 | ||
|
|
d5dbb7718d | ||
|
|
d2ad949c2e | ||
| 6811c4c249 | |||
| 3b4f98e556 | |||
| 4648507371 | |||
| 71c167308d | |||
| cb829e1922 | |||
| 5dc7ce2de7 | |||
|
|
29ec850eef |
7
.env.d.ts
vendored
Normal file
7
.env.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/// <reference types="astro/client" />
|
||||||
|
|
||||||
|
declare namespace App {
|
||||||
|
interface Locals {
|
||||||
|
canAddInventory: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
853
package-lock.json
generated
853
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
33
scripts/diagnose-join.ts
Normal file
33
scripts/diagnose-join.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import util from 'node:util';
|
||||||
|
import { client } from '../src/db/typesense.ts';
|
||||||
|
|
||||||
|
const variants = [
|
||||||
|
'$skus(*, $cards(*))',
|
||||||
|
'$skus(*,$cards(*))',
|
||||||
|
'$skus(*, card_id, $cards(*))',
|
||||||
|
'$skus(*, $cards(*, strategy:nest))',
|
||||||
|
'$skus(*, $cards(*, strategy:merge))',
|
||||||
|
];
|
||||||
|
|
||||||
|
const debug = await client.debug.retrieve();
|
||||||
|
console.log(chalk.cyan(`Typesense server version: ${debug.version}`));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
for (const include of variants) {
|
||||||
|
console.log(chalk.yellow(`include_fields: ${include}`));
|
||||||
|
try {
|
||||||
|
const res: any = await client.collections('inventories').documents().search({
|
||||||
|
q: '*',
|
||||||
|
query_by: 'content',
|
||||||
|
per_page: 1,
|
||||||
|
include_fields: include,
|
||||||
|
});
|
||||||
|
const doc = res.hits?.[0]?.document;
|
||||||
|
console.log(util.inspect(doc, { depth: null, colors: false }));
|
||||||
|
} catch (err: any) {
|
||||||
|
console.log(chalk.red(` ERROR: ${err.message ?? err}`));
|
||||||
|
}
|
||||||
|
console.log();
|
||||||
|
}
|
||||||
@@ -4,6 +4,8 @@ import type { DBInstance } from '../src/db/index.ts';
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { sql } from 'drizzle-orm'
|
import { sql } from 'drizzle-orm'
|
||||||
|
|
||||||
|
import * as util from 'util';
|
||||||
|
|
||||||
|
|
||||||
const DollarToInt = (dollar: any) => {
|
const DollarToInt = (dollar: any) => {
|
||||||
if (dollar === null) return null;
|
if (dollar === null) return null;
|
||||||
@@ -62,7 +64,7 @@ export const createCardCollection = async () => {
|
|||||||
{ name: 'releaseDate', type: 'int32' },
|
{ name: 'releaseDate', type: 'int32' },
|
||||||
{ name: 'marketPrice', type: 'int32', optional: true, sort: true },
|
{ name: 'marketPrice', type: 'int32', optional: true, sort: true },
|
||||||
{ name: 'content', type: 'string', token_separators: ['/'] },
|
{ name: 'content', type: 'string', token_separators: ['/'] },
|
||||||
{ name: 'sku_id', type: 'string[]', optional: true, reference: 'skus.id', async_reference: true }
|
// { name: 'sku_id', type: 'string[]', optional: true, reference: 'skus.id', async_reference: true }
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
console.log(chalk.green('Collection "cards" created successfully.'));
|
console.log(chalk.green('Collection "cards" created successfully.'));
|
||||||
@@ -83,6 +85,7 @@ export const createSkuCollection = async () => {
|
|||||||
{ name: 'highestPrice', type: 'int32', optional: true },
|
{ name: 'highestPrice', type: 'int32', optional: true },
|
||||||
{ name: 'lowestPrice', type: 'int32', optional: true },
|
{ name: 'lowestPrice', type: 'int32', optional: true },
|
||||||
{ name: 'marketPrice', type: 'int32', optional: true },
|
{ name: 'marketPrice', type: 'int32', optional: true },
|
||||||
|
{ name: 'card_id', type: 'string', reference: 'cards.id', async_reference: true },
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
console.log(chalk.green('Collection "skus" created successfully.'));
|
console.log(chalk.green('Collection "skus" created successfully.'));
|
||||||
@@ -101,7 +104,16 @@ export const createInventoryCollection = async () => {
|
|||||||
{ name: 'id', type: 'string' },
|
{ name: 'id', type: 'string' },
|
||||||
{ name: 'userId', type: 'string' },
|
{ name: 'userId', type: 'string' },
|
||||||
{ name: 'catalogName', type: 'string' },
|
{ name: 'catalogName', type: 'string' },
|
||||||
{ name: 'card_id', type: 'string', reference: 'cards.id' },
|
{ name: 'card_id', type: 'string', reference: 'cards.id', async_reference: true },
|
||||||
|
{ name: 'sku_id', type: 'string', reference: 'skus.id', async_reference: true },
|
||||||
|
{ name: 'purchasePrice', type: 'int32', optional: true },
|
||||||
|
// content,setName,productLineName,rarityName,energyType,cardType from cards for searching
|
||||||
|
{ name: 'content', type: 'string', token_separators: ['/'] },
|
||||||
|
{ name: 'setName', type: 'string' },
|
||||||
|
{ name: 'productLineName', type: 'string' },
|
||||||
|
{ name: 'rarityName', type: 'string' },
|
||||||
|
{ name: 'energyType', type: 'string' },
|
||||||
|
{ name: 'cardType', type: 'string' },
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
console.log(chalk.green('Collection "inventories" created successfully.'));
|
console.log(chalk.green('Collection "inventories" created successfully.'));
|
||||||
@@ -132,7 +144,7 @@ export const upsertCardCollection = async (db:DBInstance) => {
|
|||||||
content: [card.productName, card.productLineName, card.set?.setName || "", card.number, card.rarityName, card.artist || ""].join(' '),
|
content: [card.productName, card.productLineName, card.set?.setName || "", 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())
|
||||||
};
|
};
|
||||||
}), { action: 'upsert' });
|
}), { action: 'upsert' });
|
||||||
console.log(chalk.green('Collection "cards" indexed successfully.'));
|
console.log(chalk.green('Collection "cards" indexed successfully.'));
|
||||||
@@ -146,17 +158,35 @@ export const upsertSkuCollection = async (db:DBInstance) => {
|
|||||||
highestPrice: DollarToInt(sku.highestPrice),
|
highestPrice: DollarToInt(sku.highestPrice),
|
||||||
lowestPrice: DollarToInt(sku.lowestPrice),
|
lowestPrice: DollarToInt(sku.lowestPrice),
|
||||||
marketPrice: DollarToInt(sku.marketPrice),
|
marketPrice: DollarToInt(sku.marketPrice),
|
||||||
|
card_id: sku.cardId.toString(),
|
||||||
})), { action: 'upsert' });
|
})), { action: 'upsert' });
|
||||||
console.log(chalk.green('Collection "skus" indexed successfully.'));
|
console.log(chalk.green('Collection "skus" indexed successfully.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
export const upsertInventoryCollection = async (db:DBInstance) => {
|
export const upsertInventoryCollection = async (db:DBInstance) => {
|
||||||
const inv = await db.query.inventory.findMany();
|
const inv = await db.query.inventory.findMany({
|
||||||
|
with: { sku: { with: { card: { with: { set: true } } } } }
|
||||||
|
});
|
||||||
await client.collections('inventories').documents().import(inv.map(i => ({
|
await client.collections('inventories').documents().import(inv.map(i => ({
|
||||||
id: i.inventoryId,
|
id: i.inventoryId,
|
||||||
userId: i.userId,
|
userId: i.userId,
|
||||||
catalogName: i.catalogName,
|
catalogName: i.catalogName,
|
||||||
card_id: i.cardId.toString(),
|
card_id: i.sku?.cardId.toString(),
|
||||||
|
sku_id: i.skuId.toString(),
|
||||||
|
purchasePrice: DollarToInt(i.purchasePrice),
|
||||||
|
productLineName: i.sku?.card?.productLineName,
|
||||||
|
rarityName: i.sku?.card?.rarityName,
|
||||||
|
setName: i.sku?.card?.set?.setName || "",
|
||||||
|
cardType: i.sku?.card?.cardType || "",
|
||||||
|
energyType: i.sku?.card?.energyType || "",
|
||||||
|
content: [
|
||||||
|
i.sku?.card?.productName,
|
||||||
|
i.sku?.card?.productLineName,
|
||||||
|
i.sku?.card?.set?.setName || "",
|
||||||
|
i.sku?.card?.number,
|
||||||
|
i.sku?.card?.rarityName,
|
||||||
|
i.sku?.card?.artist || ""
|
||||||
|
].join(' '),
|
||||||
})), { action: 'upsert' });
|
})), { action: 'upsert' });
|
||||||
console.log(chalk.green('Collection "inventories" indexed successfully.'));
|
console.log(chalk.green('Collection "inventories" indexed successfully.'));
|
||||||
}
|
}
|
||||||
@@ -204,4 +234,7 @@ where not exists (select 1 from cards where product_id=t.product_id and variant=
|
|||||||
`);
|
`);
|
||||||
console.log(`Inserted ${inserts.rowCount} rows into cards table`);
|
console.log(`Inserted ${inserts.rowCount} rows into cards table`);
|
||||||
|
|
||||||
|
const skuUpdates = await db.execute(sql`update skus s set card_id = c.card_id from cards c where s.product_id = c.product_id and s.variant = c.variant and s.card_id is distinct from c.card_id`);
|
||||||
|
console.log(`Updated ${skuUpdates.rowCount} rows in skus table`);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -204,9 +204,9 @@ async function syncProductLine(productLine: string, field: string, fieldValue: s
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`item: ${item.setId}\tdetail: ${detailData.setId}`);
|
// console.log(`item: ${item.setId}\tdetail: ${detailData.setId}`);
|
||||||
console.log(`item: ${item.setCode}\tdetail: ${detailData.setCode}`);
|
// console.log(`item: ${item.setCode}\tdetail: ${detailData.setCode}`);
|
||||||
console.log(`item: ${item.setName}\tdetail: ${detailData.setName}`);
|
// console.log(`item: ${item.setName}\tdetail: ${detailData.setName}`);
|
||||||
// set is...
|
// set is...
|
||||||
await db.insert(schema.sets).values({
|
await db.insert(schema.sets).values({
|
||||||
setId: detailData.setId,
|
setId: detailData.setId,
|
||||||
@@ -242,7 +242,7 @@ async function syncProductLine(productLine: string, field: string, fieldValue: s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// get image if it doesn't already exist
|
// get image if it doesn't already exist
|
||||||
const imagePath = path.join(process.cwd(), 'public', 'cards', `${item.productId}.jpg`);
|
const imagePath = path.join(process.cwd(), 'static', 'cards', `${item.productId}.jpg`);
|
||||||
if (!await helper.FileExists(imagePath)) {
|
if (!await helper.FileExists(imagePath)) {
|
||||||
const imageResponse = await fetch(`https://tcgplayer-cdn.tcgplayer.com/product/${item.productId}_in_1000x1000.jpg`);
|
const imageResponse = await fetch(`https://tcgplayer-cdn.tcgplayer.com/product/${item.productId}_in_1000x1000.jpg`);
|
||||||
if (imageResponse.ok) {
|
if (imageResponse.ok) {
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import { db, ClosePool } from '../src/db/index.ts';
|
|||||||
import * as Indexing from './pokemon-helper.ts';
|
import * as Indexing from './pokemon-helper.ts';
|
||||||
|
|
||||||
|
|
||||||
//await Indexing.createCardCollection();
|
// await Indexing.createCardCollection();
|
||||||
//await Indexing.createSkuCollection();
|
await Indexing.createSkuCollection();
|
||||||
await Indexing.createInventoryCollection();
|
await Indexing.createInventoryCollection();
|
||||||
|
|
||||||
//await Indexing.upsertCardCollection(db);
|
// await Indexing.upsertCardCollection(db);
|
||||||
//await Indexing.upsertSkuCollection(db);
|
await Indexing.upsertSkuCollection(db);
|
||||||
await Indexing.upsertInventoryCollection(db);
|
await Indexing.upsertInventoryCollection(db);
|
||||||
await ClosePool();
|
await ClosePool();
|
||||||
console.log(chalk.green('Pokémon reindex complete.'));
|
console.log(chalk.green('Pokémon reindex complete.'));
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,11 @@ import Chart from 'chart.js/auto';
|
|||||||
const CONDITIONS = ["Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged"];
|
const CONDITIONS = ["Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged"];
|
||||||
|
|
||||||
const CONDITION_COLORS = {
|
const CONDITION_COLORS = {
|
||||||
"Near Mint": { active: 'rgba(156, 204, 102, 1)', muted: 'rgba(156, 204, 102, 0.67)' },
|
"Near Mint": { active: 'hsla(88, 50%, 67%, 1)', muted: 'hsla(88, 50%, 67%, 0.67)' },
|
||||||
"Lightly Played": { active: 'rgba(211, 225, 86, 1)', muted: 'rgba(211, 225, 86, 0.67)' },
|
"Lightly Played": { active: 'hsla(66, 70%, 68%, 1)', muted: 'hsla(66, 70%, 68%, 0.67)' },
|
||||||
"Moderately Played": { active: 'rgba(255, 238, 87, 1)', muted: 'rgba(255, 238, 87, 0.67)' },
|
"Moderately Played": { active: 'hsla(54, 100%, 73%, 1)', muted: 'hsla(54, 100%, 73%, 0.67)' },
|
||||||
"Heavily Played": { active: 'rgba(255, 201, 41, 1)', muted: 'rgba(255, 201, 41, 0.67)' },
|
"Heavily Played": { active: 'hsla(46, 100%, 65%, 1)', muted: 'hsla(46, 100%, 65%, 0.67)' },
|
||||||
"Damaged": { active: 'rgba(255, 167, 36, 1)', muted: 'rgba(255, 167, 36, 0.67)' },
|
"Damaged": { active: 'hsla(36, 100%, 65%, 1)', muted: 'hsla(36, 100%, 65%, 0.67)' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const RANGE_DAYS = { '1m': 30, '3m': 90, '6m': 180, '1y': 365, 'all': Infinity };
|
const RANGE_DAYS = { '1m': 30, '3m': 90, '6m': 180, '1y': 365, 'all': Infinity };
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
---
|
---
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-info p-2 rounded-circle"
|
class="btn btn-light p-2 rounded-squircle"
|
||||||
aria-label="Back to Top"
|
aria-label="Back to Top"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
id="btn-back-to-top"
|
id="btn-back-to-top"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
import BackToTop from "./BackToTop.astro"
|
import BackToTop from "./BackToTop.astro"
|
||||||
---
|
---
|
||||||
|
<div class="container-fluid container-sm mt-3">
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<div class="h5 d-none">Inventory management placeholder</div>
|
<div class="h5 d-none">Inventory management placeholder</div>
|
||||||
@@ -43,6 +44,7 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<BackToTop />
|
<BackToTop />
|
||||||
|
</div>
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
(function () {
|
(function () {
|
||||||
@@ -185,7 +187,7 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
|
|
||||||
// ── Sort dropdown ─────────────────────────────────────────────────────────
|
// ── Sort dropdown ─────────────────────────────────────────────────────────
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const btn = e.target.closest('#sortBy [data-bs-toggle="dropdown"]');
|
const btn = e.target.closest('#sortBy [data-toggle="sort-dropdown"]');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
@@ -646,6 +648,15 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
initInventoryForms();
|
initInventoryForms();
|
||||||
|
|
||||||
|
const pending = sessionStorage.getItem('pendingSearch');
|
||||||
|
if (pending) {
|
||||||
|
sessionStorage.removeItem('pendingSearch');
|
||||||
|
const input = document.getElementById('searchInput');
|
||||||
|
if (input) input.value = pending;
|
||||||
|
// The form's hx-trigger="load" will fire automatically on page load,
|
||||||
|
// picking up the pre-populated input value — no manual trigger needed.
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
---
|
---
|
||||||
|
import logo from "/src/svg/logo/rat_light.svg?raw";
|
||||||
---
|
---
|
||||||
<footer class="bd-footer py-4 py-md-5 mt-0 bg-body-tertiary">
|
<footer class="footer py-5 border-top border-subtle" role="contentinfo">
|
||||||
<div class="container py-4 py-md-5 px-4 px-md-3 text-body-secondary">
|
<div class="container">
|
||||||
<div class="row justify-content-end">
|
<div class="row g-4 mb-4">
|
||||||
<div class="col mb-3">
|
<div class="col-md-4">
|
||||||
<a class="btn btn-outline-success rounded p-2 float-end" href="/contact">
|
<a href="/" class="d-inline-block mb-3" aria-label="RAT home">
|
||||||
Contact Us
|
<span set:html={logo} class="logo-svg d-flex" style="--logo-width: 8rem;"></span>
|
||||||
<svg aria-hidden="true" class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
|
|
||||||
<path opacity=".25" d="M112 176L404 176C411.9 206.7 431 233 456.6 250.2L320 353.9L112 196.1L112 176zM112 256.3L305.5 403.1L320 414.1L334.5 403.1L509.2 270.6C515.3 271.5 521.6 272 528 272L528 464L112 464L112 256.3z"/>
|
|
||||||
<path d="M528 64C572.2 64 608 99.8 608 144C608 188.2 572.2 224 528 224C483.8 224 448 188.2 448 144C448 99.8 483.8 64 528 64zM88 128L401 128C400.3 133.2 400 138.6 400 144C400 155 401.4 165.8 404 176L112 176L112 196.1L320 353.9L456.6 250.3C472.1 260.7 489.9 267.8 509.2 270.7L334.5 403.2L320 414.2L305.5 403.2L112 256.4L112 464.1L528 464.1L528 272.1C545 272.1 561.2 268.8 576 262.8L576 512.1L64 512.1L64 128.1L88 128.1z"/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
</a>
|
||||||
|
<p class="text-body-secondary small">Real. Accurate. Transparent. Pokémon card price tracker for collectors who want to buy, sell, and trade with confidence.</p>
|
||||||
|
</div>
|
||||||
|
<nav class="col-md-2 ms-md-auto" aria-label="Tools">
|
||||||
|
<h3 class="h6 fw-semibold text-body-emphasis mb-3">Tools</h3>
|
||||||
|
<ul class="list-unstyled small text-body-secondary">
|
||||||
|
<li class="mb-2"><a href="/pokemon" class="text-body-secondary text-decoration-none hover-white">Browse Cards</a></li>
|
||||||
|
<li class="mb-2"><span class="text-body-tertiary">Inventory/Collection Tracker <em>(soon)</em></span></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<nav class="col-md-2" aria-label="Company">
|
||||||
|
<h3 class="h6 fw-semibold text-body-emphasis mb-3">Company</h3>
|
||||||
|
<ul class="list-unstyled small text-body-secondary">
|
||||||
|
<li class="mb-2"><a href="https://www.route301cards.com/" class="text-body-secondary text-decoration-none hover-white">About</a></li>
|
||||||
|
<li class="mb-2"><a href="/privacy" class="text-body-secondary text-decoration-none hover-white">Terms and Privacy</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-wrap justify-content-between align-items-center pt-4 border-top border-subtle">
|
||||||
|
<p class="text-body-tertiary small mb-0">© {new Date().getFullYear()} RAT. Not affiliated with Nintendo, The Pokémon Company, or their affiliates.</p>
|
||||||
|
<p class="text-body-tertiary small mb-0">Pokémon and all related names are trademarks of Nintendo / Creatures Inc. / GAME FREAK inc.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</footer>
|
||||||
</footer>
|
|
||||||
90
src/components/Hero.astro
Normal file
90
src/components/Hero.astro
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
import { SignInButton, Show } from '@clerk/astro/components'
|
||||||
|
import type { sign } from 'node:crypto'
|
||||||
|
---
|
||||||
|
<!-- ═══════════════════════════════════════════
|
||||||
|
HERO
|
||||||
|
═══════════════════════════════════════════ -->
|
||||||
|
<div class="hero position-relative overflow-hidden">
|
||||||
|
<div class="hero-bg" aria-hidden="true"></div>
|
||||||
|
<div class="container py-5 py-md-6 position-relative">
|
||||||
|
<div class="row align-items-center g-5">
|
||||||
|
<div class="col-12 col-xl-6">
|
||||||
|
<p class="eyebrow text-purple-light mb-3">Pokémon Card Price Aggregator</p>
|
||||||
|
<h1 class="display-4 fw-bold lh-sm mb-4">
|
||||||
|
The home of</br>
|
||||||
|
<span class="text-gradient">Real. Accurate. Transparent.</span><br/>
|
||||||
|
pricing data.
|
||||||
|
</h1>
|
||||||
|
<p class="lead text-body-secondary mb-4 pe-lg-4">
|
||||||
|
Real-time prices across the Pokémon trading card game. See prices for all conditions at a glance — no spreadsheets, no guesswork.
|
||||||
|
</p>
|
||||||
|
<div class="d-flex flex-wrap gap-3">
|
||||||
|
<Show when="signed-out">
|
||||||
|
<SignInButton asChild mode="modal">
|
||||||
|
<button class="btn btn-purple btn-lg px-4">
|
||||||
|
Get Started Free
|
||||||
|
</button>
|
||||||
|
</SignInButton>
|
||||||
|
</Show>
|
||||||
|
<Show when="signed-in">
|
||||||
|
<SignInButton asChild mode="modal">
|
||||||
|
<a href="/pokemon" class="btn btn-outline-light btn-lg px-4">Browse Cards</a>
|
||||||
|
</SignInButton>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<p class="mt-3 text-body-tertiary small d-none">Free forever. No credit card required.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-xl-6 d-none d-xl-block" aria-hidden="true">
|
||||||
|
<div class="hero-cards-mockup">
|
||||||
|
<div class="mockup-card mockup-card--1 shadow-lg rounded-4">
|
||||||
|
<img class="img-fluid" src="/static/cards/124125.jpg" alt="Sample Pokémon Card" />
|
||||||
|
</div>
|
||||||
|
<div class="mockup-card mockup-card--2 shadow-lg rounded-4">
|
||||||
|
<img class="img-fluid" src="/static/cards/88875.jpg" alt="Sample Pokémon Card" />
|
||||||
|
</div>
|
||||||
|
<div class="mockup-card mockup-card--3 shadow-lg rounded-4">
|
||||||
|
<img class="img-fluid" src="/static/cards/567429.jpg" alt="Sample Pokémon Card" />
|
||||||
|
</div>
|
||||||
|
<div class="price-chip price-chip--nm">NM <strong>$114.99</strong></div>
|
||||||
|
<div class="price-chip price-chip--lp">LP <strong>$85.66</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
// Your product image IDs
|
||||||
|
const productImages = [
|
||||||
|
"124125.jpg",
|
||||||
|
"88875.jpg",
|
||||||
|
"567429.jpg",
|
||||||
|
"88788.jpg",
|
||||||
|
"88789.jpg",
|
||||||
|
"88996.jpg",
|
||||||
|
"88997.jpg",
|
||||||
|
"189659.jpg",
|
||||||
|
"86745.jpg",
|
||||||
|
"517025.jpg",
|
||||||
|
"86911.jpg",
|
||||||
|
"87456.jpg",
|
||||||
|
"246733.jpg",
|
||||||
|
"567418.jpg",
|
||||||
|
"613917.jpg",
|
||||||
|
];
|
||||||
|
|
||||||
|
function getRandomImages(arr, count) {
|
||||||
|
const shuffled = [...arr].sort(() => 0.5 - Math.random());
|
||||||
|
return shuffled.slice(0, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
const images = document.querySelectorAll(".mockup-card img");
|
||||||
|
|
||||||
|
const selectedImages = getRandomImages(productImages, images.length);
|
||||||
|
|
||||||
|
images.forEach((img, index) => {
|
||||||
|
img.src = `/static/cards/${selectedImages[index]}`;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,12 +1,36 @@
|
|||||||
---
|
---
|
||||||
|
import { UserButton, SignInButton, Show } from '@clerk/astro/components'
|
||||||
|
import logo from "/src/svg/logo/rat_light.svg?raw";
|
||||||
---
|
---
|
||||||
<nav class="navbar navbar-expand sticky-top bg-dark" data-bs-theme="dark" aria-label="Main navigation">
|
<nav class="navbar sticky-top bg-dark shadow" data-bs-theme="dark" aria-label="Main navigation">
|
||||||
<div class="container">
|
<div class="container align-items-center" id="navContainer">
|
||||||
<a class="navbar-brand d-flex" href="/">
|
<a class="navbar-brand" href="/">
|
||||||
<span class="h3 d-none d-md-flex">Rigid's App Thing</span><span aria-hidden="true" class="h3 d-md-none d-flex m-auto">RAT</span>
|
<span set:html={logo} class="logo-svg d-flex"></span>
|
||||||
</a>
|
</a>
|
||||||
|
<div class="d-flex d-md-none nav-user-btn" id="navUserBtn">
|
||||||
|
<Show when="signed-in">
|
||||||
|
<UserButton afterSignOutUrl="/" showName={false} />
|
||||||
|
</Show>
|
||||||
|
<Show when="signed-out">
|
||||||
|
<SignInButton asChild mode="modal">
|
||||||
|
<button class="btn btn-light">Sign In</button>
|
||||||
|
</SignInButton>
|
||||||
|
</Show>
|
||||||
<slot name="navItems"/>
|
<slot name="navItems"/>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column-reverse flex-md-row search-container" id="searchContainer">
|
||||||
<slot name="searchInput"/>
|
<slot name="searchInput"/>
|
||||||
|
<div class="d-none d-md-flex ms-4 nav-user-btn">
|
||||||
|
<Show when="signed-in">
|
||||||
|
<UserButton afterSignOutUrl="/" showName={false} />
|
||||||
|
</Show>
|
||||||
|
<Show when="signed-out">
|
||||||
|
<SignInButton asChild mode="modal">
|
||||||
|
<button class="btn btn-light">Sign In</button>
|
||||||
|
</SignInButton>
|
||||||
|
</Show>
|
||||||
|
<slot name="navItems"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -1,16 +1,46 @@
|
|||||||
---
|
---
|
||||||
|
---
|
||||||
|
<button
|
||||||
|
class="navbar-toggler ms-4 p-1 btn btn-purple border-0"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="offcanvas"
|
||||||
|
data-bs-target="#navOffcanvas"
|
||||||
|
aria-controls="navOffcanvas"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
---
|
<div
|
||||||
<div class="navbar-collapse" id="navbarNav" aria-labelledby="navbarToggler">
|
id="navOffcanvasWrapper"
|
||||||
<ul class="navbar-nav ms-auto">
|
data-bs-theme="dark"
|
||||||
<li class="nav-item d-flex">
|
>
|
||||||
<a class="nav-link btn btn-warning rounded p-2" href="/pokemon" aria-label="Cards">
|
<div
|
||||||
<span class="d-inline-block d-md-none" aria-hidden="true">Cards</span>
|
class="offcanvas offcanvas-end"
|
||||||
<svg aria-hidden="true" class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
|
tabindex="-1"
|
||||||
<path opacity=".4" d="M256 519.9L256 576L576 576L576 128L378.8 128C408.7 239.7 438.6 351.3 468.5 463C397.7 482 326.8 501 256 519.9z"/>
|
id="navOffcanvas"
|
||||||
<path d="M43.5 113L352.6 30.2L468.6 462.9L159.5 545.7z"/>
|
aria-labelledby="navOffcanvasLabel"
|
||||||
</svg>
|
>
|
||||||
</a>
|
<div class="offcanvas-header">
|
||||||
|
<h5 class="offcanvas-title" id="navOffcanvasLabel">Menu</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="offcanvas-body px-3 pt-0">
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-3 border-bottom border-secondary" href="/pokemon">Browse Cards</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link py-3" href="/dashboard">Dashboard</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const wrapper = document.getElementById('navOffcanvasWrapper');
|
||||||
|
if (wrapper) document.body.appendChild(wrapper);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -8,7 +8,6 @@ import { Show } from '@clerk/astro/components'
|
|||||||
const val = Number(start.value) || 0;
|
const val = Number(start.value) || 0;
|
||||||
start.value = (val + 20).toString();
|
start.value = (val + 20).toString();
|
||||||
}
|
}
|
||||||
// delete the triggering element
|
|
||||||
if (e && e.detail && e.detail.elt) {
|
if (e && e.detail && e.detail.elt) {
|
||||||
e.detail.elt.remove();
|
e.detail.elt.remove();
|
||||||
}
|
}
|
||||||
@@ -26,21 +25,47 @@ import { Show } from '@clerk/astro/components'
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Show when="signed-in">
|
<Show when="signed-in">
|
||||||
<form class="d-flex ms-2 align-items-center gap-2" role="search" id="searchform" hx-post="/partials/cards" hx-target="#cardGrid" hx-trigger="load, submit" hx-vals='{"start":"0"}' hx-on--after-request="afterUpdate()" hx-on--before-request="beforeSearch()">
|
<form
|
||||||
<a class="btn btn-secondary btn-lg" data-bs-toggle="offcanvas" href="#filterBar" role="button" aria-controls="filterBar" aria-label="filter">
|
class="d-flex align-items-center"
|
||||||
<span class="d-block d-md-none filter-icon py-2">
|
role="search"
|
||||||
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path opacity=".4" d="M96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160L96 160z"/><path d="M66.4 147.8C71.4 135.8 83.1 128 96 128L544 128C556.9 128 568.6 135.8 573.6 147.8C578.6 159.8 575.8 173.5 566.7 182.7L384 365.3L384 544C384 556.9 376.2 568.6 364.2 573.6C352.2 578.6 338.5 575.8 329.3 566.7L265.3 502.7C259.3 496.7 255.9 488.6 255.9 480.1L256 365.3L73.4 182.6C64.2 173.5 61.5 159.7 66.4 147.8zM544 160L96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160z"/></svg>
|
id="searchform"
|
||||||
</span>
|
hx-post="/partials/cards"
|
||||||
<span class="d-none d-md-block">Filters</span>
|
hx-target="#cardGrid"
|
||||||
</a>
|
hx-trigger="load, submit"
|
||||||
|
hx-vals='{"start":"0"}'
|
||||||
|
hx-on--after-request="afterUpdate()"
|
||||||
|
hx-on--before-request="beforeSearch()"
|
||||||
|
>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
|
{Astro.url.pathname === '/pokemon' && (
|
||||||
|
<a class="btn btn-purple" data-bs-toggle="offcanvas" href="#filterBar" type="button" role="button" aria-controls="filterBar" aria-label="filter">
|
||||||
|
<span class="d-block d-md-none filter-icon py-2">
|
||||||
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M528.8 96.3C558.6 90.8 571.2 118.9 568.9 142.2C572.3 173.4 570.8 207 553.9 230.8C513.9 283.2 459.3 315.9 414.3 364.3C414.9 418.3 419.8 459.8 423.6 511.2C427.6 552.4 388.7 586.8 346.6 570.1C303.2 550.5 259.4 527.5 230.4 493.3C217 453.1 225.9 407.5 222.2 365.3C222.2 365.3 222.1 365.1 222 365C151.4 319.6 59.3 250.9 61 158.4C59.9 121 91.8 96.1 123.8 96.5C259.3 98.5 394.1 104.4 528.8 96.3zM506.1 161.4C378.3 168.2 252 162.1 125.2 160.5C128.6 227 199 270.8 250 306.8C305.5 335.4 281.6 410.5 288.3 461.7C310.8 478.9 334.6 494.6 358.9 505.8C355.4 458 350.7 415.4 350.2 364.6C349.9 349.2 355.3 333.7 366.5 321.7C384.3 302.6 402.8 287.8 421.5 270.1C446.1 245.2 477.9 225.1 499.7 196.7C509 182.2 504.7 174.5 506 161.5z"/></svg>
|
||||||
|
</span>
|
||||||
|
<span class="d-none d-md-block fw-medium">Filters</span>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
<input type="hidden" name="start" id="start" value="0" />
|
<input type="hidden" name="start" id="start" value="0" />
|
||||||
<input type="hidden" name="sort" id="sortInput" value="" />
|
<input type="hidden" name="sort" id="sortInput" value="" />
|
||||||
<input type="hidden" name="language" id="languageInput" value="all" />
|
<input type="hidden" name="language" id="languageInput" value="all" />
|
||||||
<input type="search" name="q" class="form-control form-control-lg" placeholder="Search cards..." />
|
<input type="search" name="q" id="searchInput" class="form-control search-input" placeholder="Search cards" />
|
||||||
<button type="submit" class="btn btn-danger btn-lg border border-danger-subtle border-start-0" aria-label="search" value="" onclick="const q = this.closest('form').querySelector('[name=q]').value; dataLayer.push({ event: 'view_search_results', search_term: q });">
|
<button
|
||||||
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M432 272C432 183.6 360.4 112 272 112C183.6 112 112 183.6 112 272C112 360.4 183.6 432 272 432C360.4 432 432 360.4 432 272zM401.1 435.1C365.7 463.2 320.8 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272C480 320.8 463.2 365.7 435.1 401.1L569 535C578.4 544.4 578.4 558.1 569 567.5C559.6 575.9 544.4 575.9 536 567.5L401.1 435.1z"/></svg>
|
type="submit"
|
||||||
|
class="btn btn-purple border-start-0"
|
||||||
|
aria-label="search"
|
||||||
|
onclick="
|
||||||
|
const q = this.closest('form').querySelector('[name=q]').value;
|
||||||
|
dataLayer.push({ event: 'view_search_results', search_term: q });
|
||||||
|
if (window.location.pathname !== '/pokemon') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
sessionStorage.setItem('pendingSearch', q);
|
||||||
|
window.location.href = '/pokemon';
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M503.7 304.9C520.3 80.3 214-44 100.9 169.4C-14.1 383.9 203.9 614.6 419.8 466.3C459.7 500.3 494.8 542.3 531.5 578.2C561.1 607.7 606.3 562.8 576.8 533L540 496.1C520.2 471.6 495.7 449.1 473.7 428.9C471.1 426.5 468.5 424.2 466 421.9C491.9 385.4 500.1 341 503.7 304.8zM236.1 129C334 92.1 452.1 198.1 440 298.6C440.5 404.9 335.6 462.2 244 445.8C99 407.1 100.3 178.9 236.2 129z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
</Show>
|
||||||
@@ -24,18 +24,13 @@ export const relations = defineRelations(schema, (r) => ({
|
|||||||
inventories: r.many.inventory(),
|
inventories: r.many.inventory(),
|
||||||
},
|
},
|
||||||
inventory: {
|
inventory: {
|
||||||
card: r.one.cards({
|
|
||||||
from: r.inventory.cardId,
|
|
||||||
to: r.cards.cardId,
|
|
||||||
}),
|
|
||||||
sku: r.one.skus({
|
sku: r.one.skus({
|
||||||
from: [r.inventory.cardId, r.inventory.condition],
|
from: r.inventory.skuId,
|
||||||
to: [r.skus.cardId, r.skus.condition],
|
to: r.skus.skuId,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
cards: {
|
cards: {
|
||||||
prices: r.many.skus(),
|
prices: r.many.skus(),
|
||||||
inventories: r.many.inventory(),
|
|
||||||
set: r.one.sets({
|
set: r.one.sets({
|
||||||
from: r.cards.setId,
|
from: r.cards.setId,
|
||||||
to: r.sets.setId,
|
to: r.sets.setId,
|
||||||
|
|||||||
@@ -129,16 +129,14 @@ export const inventory = pokeSchema.table('inventory',{
|
|||||||
inventoryId: uuid().primaryKey().notNull().defaultRandom(),
|
inventoryId: uuid().primaryKey().notNull().defaultRandom(),
|
||||||
userId: varchar({ length: 100 }).notNull(),
|
userId: varchar({ length: 100 }).notNull(),
|
||||||
catalogName: varchar({ length: 100 }),
|
catalogName: varchar({ length: 100 }),
|
||||||
cardId: integer().notNull(),
|
skuId: integer().notNull(),
|
||||||
condition: varchar({ length: 255 }).notNull(),
|
|
||||||
variant: varchar({ length: 100 }).default('Normal'),
|
|
||||||
quantity: integer(),
|
quantity: integer(),
|
||||||
purchasePrice: decimal({ precision: 10, scale: 2 }),
|
purchasePrice: decimal({ precision: 10, scale: 2 }),
|
||||||
note: varchar({ length:255 }),
|
note: varchar({ length:255 }),
|
||||||
createdAt: timestamp().notNull().defaultNow(),
|
createdAt: timestamp().notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
index('idx_userid_cardid').on(table.userId, table.cardId)
|
index('idx_userid_skuId').on(table.userId, table.skuId)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const processingSkus = pokeSchema.table('processing_skus', {
|
export const processingSkus = pokeSchema.table('processing_skus', {
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
---
|
---
|
||||||
import '/src/assets/css/main.scss';
|
import '/src/assets/css/main.scss';
|
||||||
|
import NavBar from '../components/NavBar.astro';
|
||||||
|
import NavItems from '../components/NavItems.astro';
|
||||||
|
import Search from '../components/Search.astro';
|
||||||
const { title } = Astro.props;
|
const { title } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -25,18 +28,18 @@ const { title } = Astro.props;
|
|||||||
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PPQMZ4PL"
|
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PPQMZ4PL"
|
||||||
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
|
||||||
<!-- End Google Tag Manager (noscript) -->
|
<!-- End Google Tag Manager (noscript) -->
|
||||||
<slot name="navbar"/>
|
<NavBar slot="navbar">
|
||||||
|
<NavItems slot="navItems" />
|
||||||
|
<Search slot="searchInput" />
|
||||||
|
</NavBar>
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
<div class="main">
|
<div class="main">
|
||||||
<div class="container-fluid container-sm mt-4">
|
|
||||||
<slot name="page"/>
|
<slot name="page"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div class="footer">
|
<div class="footer">
|
||||||
<slot name="footer"/>
|
<slot name="footer"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.9/dist/chart.umd.min.js"></script>
|
||||||
<script src="../assets/js/main.js"></script>
|
<script src="../assets/js/main.js"></script>
|
||||||
|
|||||||
@@ -1,17 +1,78 @@
|
|||||||
// src/middleware.ts
|
import { clerkMiddleware, createRouteMatcher, clerkClient } from '@clerk/astro/server';
|
||||||
import { clerkMiddleware, createRouteMatcher } from '@clerk/astro/server';
|
import type { MiddlewareNext } from 'astro';
|
||||||
import type { AstroMiddlewareRequest, AstroMiddlewareResponse } from 'astro';
|
import 'dotenv/config';
|
||||||
|
|
||||||
const isProtectedRoute = createRouteMatcher([
|
declare global {
|
||||||
'/pokemon',
|
namespace App {
|
||||||
]);
|
interface Locals {
|
||||||
|
canAddInventory: boolean;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const onRequest = clerkMiddleware((auth, context) => {
|
const isProtectedRoute = createRouteMatcher(['/pokemon']);
|
||||||
const { isAuthenticated, redirectToSignIn } = auth()
|
const isAdminRoute = createRouteMatcher(['/admin']);
|
||||||
|
|
||||||
|
const TARGET_ORG_ID = "org_3Baav9czkRLLlC7g89oJWqRRulK";
|
||||||
|
|
||||||
|
export const onRequest = clerkMiddleware(async (auth, context, next) => {
|
||||||
|
const { isAuthenticated, userId, redirectToSignIn, has } = auth();
|
||||||
|
|
||||||
if (!isAuthenticated && isProtectedRoute(context.request)) {
|
if (!isAuthenticated && isProtectedRoute(context.request)) {
|
||||||
// Add custom logic to run before redirecting
|
return redirectToSignIn();
|
||||||
|
|
||||||
return redirectToSignIn()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Inventory visibility check ──────────────────────────────────────────────
|
||||||
|
// Resolves to true if the user belongs to the target org OR has the feature
|
||||||
|
const canAddInventory = process.env.INVENTORY_ACCESS === 'true' ||
|
||||||
|
(
|
||||||
|
isAuthenticated &&
|
||||||
|
userId &&
|
||||||
|
(
|
||||||
|
!!has({ permission: "org:feature:inventory_add" }) || // Clerk feature flag
|
||||||
|
(await getUserOrgIds(context, userId)).includes(TARGET_ORG_ID)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expose the flag to your Astro pages via locals
|
||||||
|
context.locals.canAddInventory = Boolean(canAddInventory);
|
||||||
|
|
||||||
|
// ── Admin route guard (unchanged) ───────────────────────────────────────────
|
||||||
|
if (isAdminRoute(context.request)) {
|
||||||
|
if (!isAuthenticated || !userId) {
|
||||||
|
return redirectToSignIn();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = await clerkClient(context);
|
||||||
|
const memberships = await client.organizations.getOrganizationMembershipList({
|
||||||
|
organizationId: TARGET_ORG_ID,
|
||||||
|
});
|
||||||
|
|
||||||
|
const userMembership = memberships.data.find(
|
||||||
|
(m) => m.publicUserData?.userId === userId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!userMembership || userMembership.role !== "org:admin") {
|
||||||
|
return new Response(null, { status: 404 });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Clerk membership check failed:", e);
|
||||||
|
return context.redirect("/");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return next();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Helper: fetch all org IDs the current user belongs to ───────────────────
|
||||||
|
async function getUserOrgIds(context: any, userId: string): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
const client = await clerkClient(context);
|
||||||
|
const memberships = await client.users.getOrganizationMembershipList({ userId });
|
||||||
|
return memberships.data.map((m) => m.organization.id);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to fetch user org memberships:", e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
import Layout from '../layouts/Main.astro';
|
import Layout from '../layouts/Main.astro';
|
||||||
import NavItems from '../components/NavItems.astro';
|
import Search from '../components/Search.astro';
|
||||||
import NavBar from '../components/NavBar.astro';
|
import NavBar from '../components/NavBar.astro';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
import pokedexList from '../data/pokedex.json';
|
import pokedexList from '../data/pokedex.json';
|
||||||
@@ -19,12 +19,10 @@ const pokemon = pokedexList.find(p => p["#"] === randomNumber);
|
|||||||
const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
||||||
---
|
---
|
||||||
<Layout title="404 - Page Not Found">
|
<Layout title="404 - Page Not Found">
|
||||||
<NavBar slot="navbar">
|
<div class="container-fluid container-sm mt-5" slot="page">
|
||||||
<NavItems slot="navItems" />
|
<div class="row mb-4">
|
||||||
</NavBar>
|
|
||||||
<div class="row mb-4" slot="page">
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<h1 class="mb-4">404<br/>Page Not Found</h1>
|
<h1 class="mb-4">404 - Page Not Found</h1>
|
||||||
<h4>Sorry, the page you are looking for does not exist.</h4>
|
<h4>Sorry, the page you are looking for does not exist.</h4>
|
||||||
<p class="copy-big my-4">
|
<p class="copy-big my-4">
|
||||||
Return to the <a href="/">home page</a> or search for another <a href="/pokemon">Pokémon</a>.
|
Return to the <a href="/">home page</a> or search for another <a href="/pokemon">Pokémon</a>.
|
||||||
@@ -67,7 +65,7 @@ const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
|||||||
>???</h3>
|
>???</h3>
|
||||||
<button
|
<button
|
||||||
id="play-again"
|
id="play-again"
|
||||||
class="btn btn-primary mt-3 opacity-0 pokemon-transition"
|
class="btn btn-purple mt-3 opacity-0 pokemon-transition"
|
||||||
style="pointer-events: none;"
|
style="pointer-events: none;"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
@@ -76,36 +74,11 @@ const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Footer slot="footer" />
|
<Footer slot="footer" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
|
||||||
.pokemon-transition {
|
|
||||||
transition: opacity 0.4s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pokemon-clickable {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pokemon-clickable:focus-visible {
|
|
||||||
outline: 3px solid #ffc107;
|
|
||||||
outline-offset: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pokemon-pulse {
|
|
||||||
0%, 100% { filter: brightness(0) drop-shadow(0 0 6px var(--bs-info-border-subtle)); }
|
|
||||||
50% { filter: brightness(0) drop-shadow(0 0 18px var(--bs-info)); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.masked-image {
|
|
||||||
filter: brightness(0);
|
|
||||||
animation: pokemon-pulse 2s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const img = document.querySelector('.masked-image') as HTMLImageElement | null;
|
const img = document.querySelector('.masked-image') as HTMLImageElement | null;
|
||||||
const nameEl = document.querySelector('#pokemon-name');
|
const nameEl = document.querySelector('#pokemon-name');
|
||||||
|
|||||||
@@ -1,69 +1,47 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { db } from '../../db/index';
|
import { db } from '../../db/index';
|
||||||
import { inventory, skus, cards } from '../../db/schema';
|
import { inventory, priceHistory } from '../../db/schema';
|
||||||
import { client } from '../../db/typesense';
|
import { client } from '../../db/typesense';
|
||||||
import { eq, and, sql } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
|
|
||||||
const GainLoss = (purchasePrice: any, marketPrice: any) => {
|
const GainLoss = (purchasePrice: any, marketPrice: any) => {
|
||||||
if (!purchasePrice || !marketPrice) return '<div class="fs-5 fw-semibold">N/A</div>';
|
if (!purchasePrice || !marketPrice) return '<div class="fs-6 fw-semibold">N/A</div>';
|
||||||
const pp = Number(purchasePrice);
|
const pp = Number(purchasePrice);
|
||||||
const mp = Number(marketPrice);
|
const mp = Number(marketPrice);
|
||||||
if (pp === mp) return '<div class="fs-5 fw-semibold text-warning">-</div>';
|
if (pp === mp) return '<div class="fs-6 fw-semibold text-warning">-</div>';
|
||||||
if (pp > mp) return `<div class="fs-5 fw-semibold text-critical">-$${(pp - mp).toFixed(2)}</div>`;
|
if (pp > mp) return `<div class="fs-6 fw-semibold text-danger">-$${(pp - mp).toFixed(2)}</div>`;
|
||||||
return `<div class="fs-6 fw-semibold text-success">+$${(mp - pp).toFixed(2)}</div>`;
|
return `<div class="fs-6 fw-semibold text-success">+$${(mp - pp).toFixed(2)}</div>`;
|
||||||
}
|
}
|
||||||
|
const DollarToInt = (dollar: any) => {
|
||||||
|
if (dollar === null) return null;
|
||||||
|
return Math.round(dollar * 100);
|
||||||
|
}
|
||||||
|
|
||||||
const getInventory = async (userId: string, cardId: number) => {
|
const getInventory = async (userId: string, cardId: number) => {
|
||||||
|
|
||||||
const inventories = await db
|
const card = await db.query.cards.findFirst({
|
||||||
.select({
|
where: { cardId: cardId, },
|
||||||
inventoryId: inventory.inventoryId,
|
with : { prices: {
|
||||||
cardId: inventory.cardId,
|
with: { inventories: { where: { userId: userId } }, }
|
||||||
condition: inventory.condition,
|
}, },
|
||||||
variant: inventory.variant,
|
});
|
||||||
quantity: inventory.quantity,
|
|
||||||
purchasePrice: inventory.purchasePrice,
|
|
||||||
note: inventory.note,
|
|
||||||
marketPrice: skus.marketPrice,
|
|
||||||
createdAt: inventory.createdAt,
|
|
||||||
})
|
|
||||||
.from(inventory)
|
|
||||||
.leftJoin(
|
|
||||||
cards,
|
|
||||||
eq(inventory.cardId, cards.cardId)
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
skus,
|
|
||||||
and(
|
|
||||||
eq(cards.productId, skus.productId),
|
|
||||||
eq(inventory.condition, skus.condition),
|
|
||||||
eq(
|
|
||||||
sql`COALESCE(${inventory.variant}, 'Normal')`,
|
|
||||||
skus.variant
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.where(and(
|
|
||||||
eq(inventory.userId, userId),
|
|
||||||
eq(inventory.cardId, cardId)
|
|
||||||
));
|
|
||||||
|
|
||||||
const invHtml = inventories.map(inv => {
|
const invHtml = card?.prices?.flatMap(price => price.inventories.map(inv => {
|
||||||
const marketPrice = inv.marketPrice ? Number(inv.marketPrice).toFixed(2) : null;
|
const marketPrice = price.marketPrice;
|
||||||
const marketPriceDisplay = marketPrice ? `$${marketPrice}` : '—';
|
const marketPriceDisplay = marketPrice ? `$${marketPrice}` : '—';
|
||||||
const purchasePriceDisplay = inv.purchasePrice ? `$${Number(inv.purchasePrice).toFixed(2)}` : '—';
|
const purchasePriceDisplay = inv.purchasePrice ? `$${Number(inv.purchasePrice).toFixed(2)}` : '—';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="border rounded-4 p-2 inventory-entry-card"
|
<article class="border rounded-4 p-2 inventory-entry-card"
|
||||||
data-inventory-id="${inv.inventoryId}"
|
data-inventory-id="${inv.inventoryId}"
|
||||||
data-card-id="${inv.cardId}"
|
data-card-id="${price.cardId}"
|
||||||
data-purchase-price="${inv.purchasePrice}"
|
data-purchase-price="${inv.purchasePrice}"
|
||||||
data-note="${(inv.note || '').replace(/"/g, '"')}">
|
data-note="${(inv.note || '').replace(/"/g, '"')}">
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<!-- Top row -->
|
<!-- Top row -->
|
||||||
<div class="d-flex justify-content-between gap-3">
|
<div class="d-flex justify-content-between gap-3">
|
||||||
<div class="min-w-0 flex-grow-1">
|
<div class="min-w-0 flex-grow-1">
|
||||||
<div class="fw-semibold fs-6 text-body mb-1">${inv.condition}</div>
|
<div class="fw-semibold fs-6 text-body mb-1">${price.condition}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="fs-7 text-secondary">Added: ${inv.createdAt ? new Date(inv.createdAt).toLocaleDateString() : '—'}</div>
|
<div class="fs-7 text-secondary">Added: ${inv.createdAt ? new Date(inv.createdAt).toLocaleDateString() : '—'}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,13 +71,13 @@ const inventories = await db
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex align-items-center gap-2 flex-wrap">
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-inv-action="update">Edit</button>
|
<!-- <button type="button" class="btn btn-sm btn-outline-secondary" data-inv-action="update">Edit</button> -->
|
||||||
<button type="button" class="btn btn-sm btn-outline-danger" data-inv-action="remove" onclick="if(!confirm('Are you sure you want to remove this card from your inventory?')) event.stopImmediatePropagation();">Remove</button>
|
<button type="button" class="btn btn-sm btn-outline-danger" data-inv-action="remove" onclick="if(!confirm('Are you sure you want to remove this card from your inventory?')) event.stopImmediatePropagation();">Remove</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</article>`;
|
</article>`;
|
||||||
});
|
})) || [];
|
||||||
|
|
||||||
return new Response(
|
return new Response(
|
||||||
invHtml.join(''),
|
invHtml.join(''),
|
||||||
@@ -111,23 +89,48 @@ const inventories = await db
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const addToInventory = async (userId: string, cardId: number, condition: string, variant: string, purchasePrice: number, quantity: number, note: string, catalogName: string) => {
|
const addToInventory = async (userId: string, cardId: number, skuId: number, purchasePrice: number, quantity: number, note: string, catalogName: string) => {
|
||||||
|
// First add to database
|
||||||
const inv = await db.insert(inventory).values({
|
const inv = await db.insert(inventory).values({
|
||||||
userId: userId,
|
userId: userId,
|
||||||
cardId: cardId,
|
skuId: skuId,
|
||||||
catalogName: catalogName,
|
catalogName: catalogName,
|
||||||
condition: condition,
|
purchasePrice: purchasePrice.toFixed(2),
|
||||||
variant: variant,
|
|
||||||
purchasePrice: purchasePrice,
|
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
note: note,
|
note: note,
|
||||||
}).returning();
|
}).returning();
|
||||||
|
// Get card details from the database to add to Typesense
|
||||||
|
const card = await db.query.cards.findFirst({
|
||||||
|
where: { cardId: cardId },
|
||||||
|
with: { set: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// And then add to Typesense for searching
|
||||||
await client.collections('inventories').documents().import(inv.map(i => ({
|
await client.collections('inventories').documents().import(inv.map(i => ({
|
||||||
id: i.inventoryId,
|
id: i.inventoryId,
|
||||||
userId: i.userId,
|
userId: i.userId,
|
||||||
catalogName: i.catalogName,
|
catalogName: i.catalogName,
|
||||||
card_id: i.cardId.toString(),
|
sku_id: i.skuId.toString(),
|
||||||
|
purchasePrice: DollarToInt(i.purchasePrice),
|
||||||
|
productLineName: card?.productLineName,
|
||||||
|
rarityName: card?.rarityName,
|
||||||
|
setName: card?.set?.setName || "",
|
||||||
|
cardType: card?.cardType || "",
|
||||||
|
energyType: card?.energyType || "",
|
||||||
|
card_id: card?.cardId.toString() || "",
|
||||||
|
content: [
|
||||||
|
card?.productName,
|
||||||
|
card?.productLineName,
|
||||||
|
card?.set?.setName || "",
|
||||||
|
card?.number,
|
||||||
|
card?.rarityName,
|
||||||
|
card?.artist || ""
|
||||||
|
].join(' '),
|
||||||
})));
|
})));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding inventory to Typesense:', error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFromInventory = async (inventoryId: string) => {
|
const removeFromInventory = async (inventoryId: string) => {
|
||||||
@@ -136,14 +139,17 @@ const removeFromInventory = async (inventoryId: string) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const updateInventory = async (inventoryId: string, quantity: number, purchasePrice: number, note: string) => {
|
const updateInventory = async (inventoryId: string, quantity: number, purchasePrice: number, note: string) => {
|
||||||
|
// Update the database
|
||||||
await db.update(inventory).set({
|
await db.update(inventory).set({
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
purchasePrice: purchasePrice,
|
purchasePrice: purchasePrice.toFixed(2),
|
||||||
note: note,
|
note: note,
|
||||||
}).where(eq(inventory.inventoryId, inventoryId));
|
}).where(eq(inventory.inventoryId, inventoryId));
|
||||||
|
// No need to update Typesense since we don't search by quantity or price
|
||||||
}
|
}
|
||||||
|
|
||||||
export const POST: APIRoute = async ({ request, locals }) => {
|
export const POST: APIRoute = async ({ request, locals }) => {
|
||||||
|
// Access form data from the request body
|
||||||
const formData = await request.formData();
|
const formData = await request.formData();
|
||||||
const action = formData.get('action');
|
const action = formData.get('action');
|
||||||
const cardId = Number(formData.get('cardId')) || 0;
|
const cardId = Number(formData.get('cardId')) || 0;
|
||||||
@@ -152,13 +158,19 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
switch (action) {
|
switch (action) {
|
||||||
|
|
||||||
case 'add':
|
case 'add':
|
||||||
const condition = formData.get('condition')?.toString() || 'Unknown';
|
|
||||||
const variant = formData.get('variant')?.toString() || 'Normal';
|
|
||||||
const purchasePrice = Number(formData.get('purchasePrice')) || 0;
|
const purchasePrice = Number(formData.get('purchasePrice')) || 0;
|
||||||
const quantity = Number(formData.get('quantity')) || 1;
|
const quantity = Number(formData.get('quantity')) || 1;
|
||||||
const note = formData.get('note')?.toString() || '';
|
const note = formData.get('note')?.toString() || '';
|
||||||
const catalogName = formData.get('catalogName')?.toString() || 'Default';
|
const catalogName = formData.get('catalogName')?.toString() || 'Default';
|
||||||
await addToInventory(userId!, cardId, condition, variant, purchasePrice, quantity, note, catalogName);
|
const condition = formData.get('condition')?.toString() || 'Near Mint';
|
||||||
|
const skuId = await db.query.skus.findFirst({
|
||||||
|
where: { cardId: cardId, condition: condition },
|
||||||
|
columns: { skuId: true },
|
||||||
|
}).then(sku => sku?.skuId);
|
||||||
|
if (!skuId) {
|
||||||
|
return new Response('SKU not found for card', { status: 404 });
|
||||||
|
}
|
||||||
|
await addToInventory(userId!, cardId, skuId, purchasePrice, quantity, note, catalogName);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'remove':
|
case 'remove':
|
||||||
@@ -175,8 +187,10 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
// No action = list inventory for this card
|
||||||
return getInventory(userId!, cardId);
|
return getInventory(userId!, cardId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always return current inventory after a mutation
|
||||||
return getInventory(userId!, cardId);
|
return getInventory(userId!, cardId);
|
||||||
};
|
};
|
||||||
@@ -1,15 +1,11 @@
|
|||||||
---
|
---
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
import Layout from '../layouts/Main.astro';
|
import Layout from '../layouts/Main.astro';
|
||||||
import NavItems from '../components/NavItems.astro';
|
|
||||||
import NavBar from '../components/NavBar.astro';
|
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
---
|
---
|
||||||
<Layout title="Contact Us">
|
<Layout title="Contact Us">
|
||||||
<NavBar slot="navbar">
|
<div class="container-fluid container-sm my-5" slot="page">
|
||||||
<NavItems slot="navItems" />
|
<div class="row mb-4">
|
||||||
</NavBar>
|
|
||||||
<div class="row mb-4" slot="page">
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<h1>Contact Us</h1>
|
<h1>Contact Us</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +38,7 @@ import Footer from '../components/Footer.astro';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit button -->
|
<!-- Submit button -->
|
||||||
<button type="submit" class="btn btn-light" id="submitBtn">Submit</button>
|
<button type="submit" class="btn btn-purple" id="submitBtn">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Hidden iframe absorbs the Google Forms redirect -->
|
<!-- Hidden iframe absorbs the Google Forms redirect -->
|
||||||
@@ -54,6 +50,7 @@ import Footer from '../components/Footer.astro';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<Footer slot="footer" />
|
<Footer slot="footer" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
|||||||
@@ -1,321 +1,42 @@
|
|||||||
---
|
---
|
||||||
import Layout from "../layouts/Main.astro";
|
import Layout from "../layouts/Main.astro";
|
||||||
import NavBar from "../components/NavBar.astro";
|
|
||||||
import NavItems from "../components/NavItems.astro";
|
|
||||||
import Footer from "../components/Footer.astro";
|
import Footer from "../components/Footer.astro";
|
||||||
|
import BackToTop from "../components/BackToTop.astro";
|
||||||
import FirstEditionIcon from "../components/FirstEditionIcon.astro";
|
import FirstEditionIcon from "../components/FirstEditionIcon.astro";
|
||||||
|
import { db } from '../db/index';
|
||||||
|
import { inventory, skus } from '../db/schema';
|
||||||
|
import { sql, sum, eq } from "drizzle-orm";
|
||||||
|
|
||||||
// Mock inventory using the same schema as the Typesense cards collection.
|
|
||||||
// skus mirror the real shape: marketPrice is in cents (÷100 = dollars).
|
|
||||||
const inventory = [
|
|
||||||
{
|
|
||||||
productId: "42382",
|
|
||||||
productName: "Charizard",
|
|
||||||
setName: "Base Set",
|
|
||||||
setCode: "BS",
|
|
||||||
number: "4/102",
|
|
||||||
rarityName: "Rare Holo",
|
|
||||||
energyType: "Fire",
|
|
||||||
variant: "1st Edition",
|
|
||||||
qty: 2,
|
|
||||||
purchasePrice: 32000,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 40000 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 31000 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 22000 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 14000 },
|
|
||||||
{ condition: "Damaged", marketPrice: 8500 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "146682",
|
|
||||||
productName: "Pikachu",
|
|
||||||
setName: "Shining Legends",
|
|
||||||
setCode: "SLG",
|
|
||||||
number: "SM70",
|
|
||||||
rarityName: "Promo",
|
|
||||||
energyType: "Lightning",
|
|
||||||
variant: "Normal",
|
|
||||||
qty: 5,
|
|
||||||
purchasePrice: 1500,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 2000 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 1500 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 1100 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 700 },
|
|
||||||
{ condition: "Damaged", marketPrice: 400 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "246723",
|
|
||||||
productName: "Umbreon VMAX",
|
|
||||||
setName: "Evolving Skies",
|
|
||||||
setCode: "EVS",
|
|
||||||
number: "215/203",
|
|
||||||
rarityName: "Secret Rare",
|
|
||||||
energyType: "Darkness",
|
|
||||||
variant: "Alternate Art",
|
|
||||||
qty: 1,
|
|
||||||
purchasePrice: 8500,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 11500 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 9000 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 6500 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 4000 },
|
|
||||||
{ condition: "Damaged", marketPrice: 2000 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "197660",
|
|
||||||
productName: "Gyarados",
|
|
||||||
setName: "Hidden Fates",
|
|
||||||
setCode: "HIF",
|
|
||||||
number: "SV19/SV94",
|
|
||||||
rarityName: "Shiny Holo Rare",
|
|
||||||
energyType: "Water",
|
|
||||||
variant: "Shiny",
|
|
||||||
qty: 3,
|
|
||||||
purchasePrice: 2500,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 3000 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 2300 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 1600 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 900 },
|
|
||||||
{ condition: "Damaged", marketPrice: 500 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "246733",
|
|
||||||
productName: "Rayquaza VMAX",
|
|
||||||
setName: "Evolving Skies",
|
|
||||||
setCode: "EVS",
|
|
||||||
number: "218/203",
|
|
||||||
rarityName: "Secret Rare",
|
|
||||||
energyType: "Dragon",
|
|
||||||
variant: "Alternate Art",
|
|
||||||
qty: 2,
|
|
||||||
purchasePrice: 6500,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 8800 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 7000 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 5000 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 3200 },
|
|
||||||
{ condition: "Damaged", marketPrice: 1800 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "264218",
|
|
||||||
productName: "Eevee",
|
|
||||||
setName: "Sword & Shield",
|
|
||||||
setCode: "SSH",
|
|
||||||
number: "TG07/TG30",
|
|
||||||
rarityName: "Trainer Gallery",
|
|
||||||
energyType: "Colorless",
|
|
||||||
variant: "Normal",
|
|
||||||
qty: 10,
|
|
||||||
purchasePrice: 800,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 900 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 700 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 500 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 300 },
|
|
||||||
{ condition: "Damaged", marketPrice: 150 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "451834",
|
|
||||||
productName: "Lugia V",
|
|
||||||
setName: "Silver Tempest",
|
|
||||||
setCode: "SIT",
|
|
||||||
number: "186/195",
|
|
||||||
rarityName: "Ultra Rare",
|
|
||||||
energyType: "Colorless",
|
|
||||||
variant: "Alternate Art",
|
|
||||||
qty: 1,
|
|
||||||
purchasePrice: 4500,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 5800 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 4600 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 3200 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 2000 },
|
|
||||||
{ condition: "Damaged", marketPrice: 1000 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "106997",
|
|
||||||
productName: "Blastoise",
|
|
||||||
setName: "Base Set",
|
|
||||||
setCode: "BS",
|
|
||||||
number: "2/102",
|
|
||||||
rarityName: "Rare Holo",
|
|
||||||
energyType: "Water",
|
|
||||||
variant: "Shadowless",
|
|
||||||
qty: 1,
|
|
||||||
purchasePrice: 18000,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 24000 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 18500 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 13000 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 8000 },
|
|
||||||
{ condition: "Damaged", marketPrice: 4500 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "253265",
|
|
||||||
productName: "Espeon VMAX",
|
|
||||||
setName: "Evolving Skies",
|
|
||||||
setCode: "EVS",
|
|
||||||
number: "205/203",
|
|
||||||
rarityName: "Secret Rare",
|
|
||||||
energyType: "Psychic",
|
|
||||||
variant: "Alternate Art",
|
|
||||||
qty: 2,
|
|
||||||
purchasePrice: 7000,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 9200 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 7300 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 5200 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 3300 },
|
|
||||||
{ condition: "Damaged", marketPrice: 1600 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "253266",
|
|
||||||
productName: "Gengar VMAX",
|
|
||||||
setName: "Fusion Strike",
|
|
||||||
setCode: "FST",
|
|
||||||
number: "271/264",
|
|
||||||
rarityName: "Secret Rare",
|
|
||||||
energyType: "Psychic",
|
|
||||||
variant: "Alternate Art",
|
|
||||||
qty: 1,
|
|
||||||
purchasePrice: 5500,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 4800 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 3800 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 2700 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 1700 },
|
|
||||||
{ condition: "Damaged", marketPrice: 900 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "226432",
|
|
||||||
productName: "Pikachu VMAX",
|
|
||||||
setName: "Vivid Voltage",
|
|
||||||
setCode: "VIV",
|
|
||||||
number: "188/185",
|
|
||||||
rarityName: "Secret Rare",
|
|
||||||
energyType: "Lightning",
|
|
||||||
variant: "Rainbow Rare",
|
|
||||||
qty: 3,
|
|
||||||
purchasePrice: 3200,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 4100 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 3200 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 2300 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 1400 },
|
|
||||||
{ condition: "Damaged", marketPrice: 750 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "253275",
|
|
||||||
productName: "Mew VMAX",
|
|
||||||
setName: "Fusion Strike",
|
|
||||||
setCode: "FST",
|
|
||||||
number: "269/264",
|
|
||||||
rarityName: "Secret Rare",
|
|
||||||
energyType: "Psychic",
|
|
||||||
variant: "Alternate Art",
|
|
||||||
qty: 2,
|
|
||||||
purchasePrice: 4200,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 5600 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 4400 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 3100 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 2000 },
|
|
||||||
{ condition: "Damaged", marketPrice: 1000 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "478077",
|
|
||||||
productName: "Darkrai VSTAR",
|
|
||||||
setName: "Astral Radiance",
|
|
||||||
setCode: "ASR",
|
|
||||||
number: "189/189",
|
|
||||||
rarityName: "Secret Rare",
|
|
||||||
energyType: "Darkness",
|
|
||||||
variant: "Gold",
|
|
||||||
qty: 1,
|
|
||||||
purchasePrice: 3800,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 3200 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 2500 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 1800 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 1100 },
|
|
||||||
{ condition: "Damaged", marketPrice: 600 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "477060",
|
|
||||||
productName: "Leafeon VSTAR",
|
|
||||||
setName: "Pokémon GO",
|
|
||||||
setCode: "PGO",
|
|
||||||
number: "076/078",
|
|
||||||
rarityName: "Ultra Rare",
|
|
||||||
energyType: "Grass",
|
|
||||||
variant: "Normal",
|
|
||||||
qty: 4,
|
|
||||||
purchasePrice: 1200,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 1800 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 1400 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 1000 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 600 },
|
|
||||||
{ condition: "Damaged", marketPrice: 300 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
productId: "478100",
|
|
||||||
productName: "Giratina VSTAR",
|
|
||||||
setName: "Lost Origin",
|
|
||||||
setCode: "LOR",
|
|
||||||
number: "196/196",
|
|
||||||
rarityName: "Secret Rare",
|
|
||||||
energyType: "Dragon",
|
|
||||||
variant: "Alternate Art",
|
|
||||||
qty: 1,
|
|
||||||
purchasePrice: 5200,
|
|
||||||
skus: [
|
|
||||||
{ condition: "Near Mint", marketPrice: 7800 },
|
|
||||||
{ condition: "Lightly Played", marketPrice: 6100 },
|
|
||||||
{ condition: "Moderately Played", marketPrice: 4400 },
|
|
||||||
{ condition: "Heavily Played", marketPrice: 2800 },
|
|
||||||
{ condition: "Damaged", marketPrice: 1400 },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// Helpers
|
|
||||||
const nmPrice = (card: typeof inventory[0]) => (card.skus[0]?.marketPrice ?? 0) / 100;
|
|
||||||
const nmPurchase = (card: typeof inventory[0]) => card.purchasePrice / 100;
|
|
||||||
const gain = (card: typeof inventory[0]) => nmPrice(card) - nmPurchase(card);
|
|
||||||
|
|
||||||
const totalQty = inventory.reduce((s, c) => s + c.qty, 0);
|
const { userId } = Astro.locals.auth();
|
||||||
const totalValue = inventory.reduce((s, c) => s + nmPrice(c) * c.qty, 0);
|
|
||||||
const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
|
const summary = await db
|
||||||
|
.select({
|
||||||
|
totalQty: sum(inventory.quantity).mapWith(Number),
|
||||||
|
totalValue: sum(sql`(${inventory.quantity} * ${skus.marketPrice})`).mapWith(Number),
|
||||||
|
totalGain: sum(sql`(${inventory.quantity} * (${skus.marketPrice} - ${inventory.purchasePrice}))`).mapWith(Number),
|
||||||
|
})
|
||||||
|
.from(inventory)
|
||||||
|
.innerJoin(skus, eq(inventory.skuId, skus.skuId))
|
||||||
|
.where(eq(inventory.userId, userId!))
|
||||||
|
.execute()
|
||||||
|
.then(res => res[0]);
|
||||||
|
const totalQty = summary.totalQty || 0;
|
||||||
|
const totalValue = summary.totalValue || 0;
|
||||||
|
const totalGain = summary.totalGain || 0;
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Inventory Dashboard">
|
<Layout title="Inventory Dashboard">
|
||||||
<NavBar slot="navbar">
|
<div class="container-fluid container-sm mt-3" slot="page">
|
||||||
<NavItems slot="navItems" />
|
<BackToTop />
|
||||||
</NavBar>
|
<div class="row mb-4">
|
||||||
|
|
||||||
<div class="row g-0" style="min-height: calc(100vh - 120px)" slot="page">
|
|
||||||
<aside class="col-12 col-md-2 border-end border-secondary bg-dark p-3 d-flex flex-column gap-3">
|
<aside class="col-12 col-md-2 border-end border-secondary bg-dark p-3 d-flex flex-column gap-3">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
<h6 class="mb-0 text-uppercase text-secondary fw-bold ls-wide" style="letter-spacing:.08em">Catalogs</h6>
|
<h6 class="mb-0 text-uppercase text-secondary fw-bold ls-wide" style="letter-spacing:.08em">Catalogs</h6>
|
||||||
<button
|
<button
|
||||||
class="btn btn-sm btn-success fs-7"
|
class="btn btn-purple-secondary fs-7"
|
||||||
title="New catalog"
|
title="New catalog"
|
||||||
type="button"
|
type="button"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
@@ -325,7 +46,7 @@ const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
|
|||||||
|
|
||||||
<ul id="catalogList" class="list-group list-group-flush">
|
<ul id="catalogList" class="list-group list-group-flush">
|
||||||
<li
|
<li
|
||||||
class="list-group-item list-group-item-action bg-transparent text-light border-0 rounded px-2 py-2 d-flex align-items-center justify-content-between active"
|
class="list-group-item list-group-item-action fw-semibold border-0 rounded p-2 d-flex align-items-center active"
|
||||||
data-catalog="all"
|
data-catalog="all"
|
||||||
role="button"
|
role="button"
|
||||||
style="cursor:pointer"
|
style="cursor:pointer"
|
||||||
@@ -333,7 +54,7 @@ const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
|
|||||||
<span class="d-flex align-items-center gap-2">
|
<span class="d-flex align-items-center gap-2">
|
||||||
View all cards
|
View all cards
|
||||||
</span>
|
</span>
|
||||||
<span class="badge rounded-pill text-bg-secondary small">{totalQty}</span>
|
<span class="badge rounded-pill text-bg-secondary small ms-auto">{totalQty}</span>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
{["Case Cards", "Japanese Singles", "Bulk"].map((name) => (
|
{["Case Cards", "Japanese Singles", "Bulk"].map((name) => (
|
||||||
@@ -351,7 +72,7 @@ const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
|
|||||||
<div class="mt-auto pt-3 border-top border-secondary small text-secondary">
|
<div class="mt-auto pt-3 border-top border-secondary small text-secondary">
|
||||||
<div class="d-flex justify-content-between mb-1"><span>Total Cards</span><span class="text-light fw-semibold">{totalQty}</span></div>
|
<div class="d-flex justify-content-between mb-1"><span>Total Cards</span><span class="text-light fw-semibold">{totalQty}</span></div>
|
||||||
<div class="d-flex justify-content-between mb-1"><span>Market Value</span><span class="text-success fw-semibold">${totalValue.toFixed(0)}</span></div>
|
<div class="d-flex justify-content-between mb-1"><span>Market Value</span><span class="text-success fw-semibold">${totalValue.toFixed(0)}</span></div>
|
||||||
<div class="d-flex justify-content-between"><span>Profit/Loss</span><span class={`fw-semibold ${totalGain >= 0 ? "text-success" : "text-danger"}`}>{totalGain >= 0 ? "+" : ""}${Math.abs(totalGain).toFixed(0)}</span></div>
|
<div class="d-flex justify-content-between"><span>Gain/Loss</span><span class={`fw-semibold ${totalGain >= 0 ? "text-success" : "text-danger"}`}>{totalGain >= 0 ? "+" : ""}${Math.abs(totalGain).toFixed(0)}</span></div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
@@ -359,119 +80,58 @@ const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
|
|||||||
<div class="d-flex flex-wrap gap-2 align-items-center mb-4">
|
<div class="d-flex flex-wrap gap-2 align-items-center mb-4">
|
||||||
<div class="d-flex align-items-center gap-1">
|
<div class="d-flex align-items-center gap-1">
|
||||||
<button id="btnGrid" type="button" class="btn btn-sm btn-link text-secondary px-1 view-toggle-btn active" title="Images view">
|
<button id="btnGrid" type="button" class="btn btn-sm btn-link text-secondary px-1 view-toggle-btn active" title="Images view">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="1.5rem" height="1.5rem" fill="currentColor"><path d="M351.6 220.4C349.7 258.3 381.1 288.3 418 286.5L527.6 281.1C593 275.5 589.7 214.9 589 162.5C593.9 90.4 588.4 39.6 502.4 44.6C391.5 42.2 352.5 36.7 354.5 164.3C353.4 183.3 352.4 202.4 351.5 220.3zM418.4 168.6L418.4 168.6C419.6 147.4 420.8 125.9 421.6 109.4C421.6 109.2 421.6 109.1 421.7 109C421.7 108.8 422 108.6 422.1 108.6C456.2 108.5 491.4 108.6 525.7 108.7C525.8 108.7 526 109 526.1 109.1L526.1 109.3C525.5 142.4 524.9 184.6 524.2 217.3L415.6 222.6C416.3 207.3 417.4 188.1 418.5 168.7zM301.4 112.5C303.3 74.7 272 44.6 235 46.4L125.4 51.8C40.8 58.8 69.2 164.2 63.1 222.4C62.4 258.3 91.2 288.3 127.5 288.3C159.4 288.3 198.3 288.3 231 288.3C298.6 286.3 297.2 220.3 298.5 168.6C299.6 149.6 300.6 130.5 301.5 112.6zM234.6 164.3C233.4 185.6 232.2 207 231.4 223.5C231.4 223.7 231.4 223.8 231.3 223.9C231.3 224 231.1 224.2 231.1 224.2L231 224.3C198.3 224.2 159.3 224.3 127.3 224.3C127.3 224.3 127.2 224.3 127.1 224.2C127 224.1 126.9 224 126.9 224L126.9 223.8C127.5 192.1 128.1 149.9 128.8 115.8L237.4 110.5C236.7 125.8 235.6 145 234.5 164.4zM63.6 404C64.5 421.9 65.5 441 66.6 460C68.2 512.2 65.9 577.1 134.1 579.7L237.6 579.7C273.9 579.7 302.7 549.7 302 513.9C301.7 498.8 301.4 480.4 301.1 461.9C301.8 409.5 305 348.9 239.7 343.3L130 338C93 336.2 61.7 366.2 63.6 404.1zM130.4 455.8C129.3 436.4 128.3 417.1 127.5 401.8L236.1 407.1C236.8 441.2 237.3 483.4 238 515C238 515.1 238 515.1 238 515.2C238 515.3 237.9 515.4 237.8 515.5C237.7 515.6 237.6 515.6 237.6 515.6C205.8 515.6 166.4 515.5 134 515.6C133.9 515.6 133.7 515.3 133.7 515.2C133.6 515.1 133.6 515 133.6 514.8C132.8 498.3 131.6 476.9 130.4 455.6L130.4 455.6zM523 578.1C560 579.9 591.3 549.8 589.4 512C588.5 494.1 587.5 475 586.4 456C585.9 381.4 580.2 328.2 490 336.3C457.9 336.4 439 336.3 415.4 336.3C379.1 336.3 350.3 366.3 351 402.1C351.3 417.2 351.6 435.6 351.9 454.1C351.2 506.5 348 567.1 413.3 572.7L523 578.1zM525.5 514.1L416.9 508.8C416.2 474.7 415.7 432.5 415 400.9L415 400.7C415 400.6 415.3 400.3 415.3 400.3C443.6 400.3 484.7 400.3 518.9 400.3C518.9 400.3 519 400.3 519.1 400.4C519.2 400.5 519.3 400.6 519.3 400.7C519.4 400.8 519.4 400.9 519.4 401.1C521.1 435.4 524 484.9 525.4 514.3z"/></svg>
|
||||||
<path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h3A1.5 1.5 0 0 1 7 2.5v3A1.5 1.5 0 0 1 5.5 7h-3A1.5 1.5 0 0 1 1 5.5v-3zm8 0A1.5 1.5 0 0 1 10.5 1h3A1.5 1.5 0 0 1 15 2.5v3A1.5 1.5 0 0 1 13.5 7h-3A1.5 1.5 0 0 1 9 5.5v-3zm-8 8A1.5 1.5 0 0 1 2.5 9h3A1.5 1.5 0 0 1 7 10.5v3A1.5 1.5 0 0 1 5.5 15h-3A1.5 1.5 0 0 1 1 13.5v-3zm8 0A1.5 1.5 0 0 1 10.5 9h3A1.5 1.5 0 0 1 15 10.5v3A1.5 1.5 0 0 1 13.5 15h-3A1.5 1.5 0 0 1 9 13.5v-3z"/>
|
<span class="ms-1">Grid</span>
|
||||||
</svg>
|
|
||||||
<span class="small ms-1">Images</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button id="btnTable" type="button" class="btn btn-sm btn-link text-secondary px-1 view-toggle-btn" title="List view">
|
<button id="btnTable" type="button" class="btn btn-sm btn-link text-secondary px-1 view-toggle-btn" title="List view">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" width="1.5rem" height="1.5rem" fill="currentColor" style="d-inline"><path d="M102.3 211.8C110.7 214 118.9 213.7 126.2 210C132.6 208 138.3 204 142.4 198.4C184.2 169.6 148.5 98.8 98.3 108.3C39.6 119.9 49.3 205.8 102.2 211.8zM114.4 375.6C114.4 375.6 114.8 375.5 116.2 375.3L116.5 375.2C169.2 368.9 178.6 283.3 120.1 271.7C69.9 262.3 34.2 332.9 76 361.8C80.1 367.4 85.9 371.4 92.3 373.4C99 376.6 106.8 377.6 114.5 375.5zM116.8 423.5C63.5 413.9 30 495.4 84.4 522.3C130.5 544.4 183.2 485.4 150.4 446.7C147.9 440.2 143.4 434.9 137.7 431.2C132.1 426.4 124.8 423.5 116.8 423.5zM352.8 508.4C423.1 503.6 491.2 499 561.5 501.8C579.2 502.5 594.1 488.8 594.8 471.1C595.5 453.4 581.7 438.6 564.1 437.9C490.4 435 416.4 439.9 344.2 444.8C310.8 447 277.8 449.3 245.4 450.7C227.7 451.4 214 466.4 214.8 484C215.5 501.7 230.5 515.4 248.1 514.6C283.8 513 318.6 510.7 352.8 508.4zM344 344.8L344.3 344.8C412.9 343.4 479.9 342.9 548.3 346.2C566 347 580.9 333.3 581.7 315.6C582.5 298 568.8 283 551.1 282.2C482.2 278.8 412.2 279.3 343.1 280.7C310.3 281.3 277.9 281.9 245.8 281.9C228.1 281.9 213.8 296.2 213.8 313.9C213.8 331.6 228.1 345.9 245.8 345.9C278.5 345.9 311.4 345.3 343.9 344.7zM444.8 187.4C480.8 186.5 519 181.7 551.8 187.6C569.2 190.7 585.8 179.1 588.9 161.7C592 144.3 580.4 127.7 563 124.6C484.4 115.2 408.3 126.5 331 129.9C301.8 131.9 273.7 133 246 131.4C228.4 130.3 213.2 143.8 212.2 161.4C213.8 217.2 300.2 190.8 335.5 193.7C372.2 191.2 408.7 188.4 444.8 187.4z"/></svg>
|
||||||
<path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5z"/>
|
<span class="ms-1">List</span>
|
||||||
</svg>
|
|
||||||
<span class="small ms-1">List</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="vr opacity-25 mx-1"></div>
|
<div class="vr opacity-25 mx-1"></div>
|
||||||
|
|
||||||
<button
|
<a href="/pokemon" class="btn btn-vendor">+ Add Card</a>
|
||||||
type="button"
|
|
||||||
class="btn btn-sm btn-vendor"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#addCardModal"
|
|
||||||
>+ Add Card</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="btn btn-sm btn-outline-light"
|
class="btn btn-secondary"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#bulkImportModal"
|
data-bs-target="#bulkImportModal"
|
||||||
>Bulk Import</button>
|
>Bulk Import</button>
|
||||||
|
|
||||||
<div class="ms-auto position-relative">
|
<div class="ms-auto position-relative">
|
||||||
<input
|
<div class="input-group">
|
||||||
id="inventorySearch"
|
<input type="hidden" name="start" id="start" value="0" />
|
||||||
class="form-control form-control-sm bg-dark text-light border-secondary"
|
<input type="hidden" name="sort" id="sortInput" value="" />
|
||||||
placeholder="Search inventory…"
|
<input type="hidden" name="language" id="languageInput" value="all" />
|
||||||
style="min-width:200px; padding-right:2rem"
|
<input type="search" name="i" id="searchInput" class="form-control search-input" placeholder="Search your inventory" />
|
||||||
/>
|
|
||||||
<button
|
<button
|
||||||
id="clearSearch"
|
type="submit"
|
||||||
type="button"
|
class="btn btn-purple border-start-0"
|
||||||
class="btn btn-sm p-0 position-absolute top-50 end-0 translate-middle-y me-2 text-secondary d-none"
|
aria-label="search"
|
||||||
style="line-height:1"
|
onclick="
|
||||||
aria-label="Clear search"
|
const i = this.closest('form').querySelector('[name=i]').value;
|
||||||
>✕</button>
|
dataLayer.push({ event: 'view_inventory_results', search_term: i });
|
||||||
|
if (window.location.pathname !== '/dashboard') {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
sessionStorage.setItem('pendingSearch', 1);
|
||||||
|
window.location.href = '/dashboard';
|
||||||
|
}
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M503.7 304.9C520.3 80.3 214-44 100.9 169.4C-14.1 383.9 203.9 614.6 419.8 466.3C459.7 500.3 494.8 542.3 531.5 578.2C561.1 607.7 606.3 562.8 576.8 533L540 496.1C520.2 471.6 495.7 449.1 473.7 428.9C471.1 426.5 468.5 424.2 466 421.9C491.9 385.4 500.1 341 503.7 304.8zM236.1 129C334 92.1 452.1 198.1 440 298.6C440.5 404.9 335.6 462.2 244 445.8C99 407.1 100.3 178.9 236.2 129z"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="inventoryView">
|
<div id="inventoryView">
|
||||||
<div id="gridView" class="row g-4 row-cols-2 row-cols-md-3 row-cols-xl-4 row-cols-xxxl-5">
|
<div id="gridView" class="row g-4 row-cols-2 row-cols-md-3 row-cols-xl-4 row-cols-xxxl-5" hx-post="/partials/inventory-cards" hx-trigger="load">
|
||||||
{inventory.map(card => {
|
|
||||||
const market = nmPrice(card);
|
|
||||||
const purchase = nmPurchase(card);
|
|
||||||
const diff = market - purchase;
|
|
||||||
const pct = purchase > 0 ? (diff / purchase) * 100 : 0;
|
|
||||||
const isGain = diff >= 0;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="col">
|
|
||||||
<article class="inv-grid-card">
|
|
||||||
<div
|
|
||||||
class="card-trigger position-relative inv-grid-media"
|
|
||||||
data-card-id={card.productId}
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#cardModal"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="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}
|
|
||||||
loading="lazy"
|
|
||||||
decoding="async"
|
|
||||||
class="img-fluid rounded-4 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" style="z-index:4">
|
|
||||||
<FirstEditionIcon edition={card.variant} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-row justify-content-between my-1 align-items-center">
|
|
||||||
<input type="number" class="form-control text-center" style="max-width: 50%;" value="1" min="1" max="999" aria-label="Quantity input" aria-describedby="button-minus button-plus">
|
|
||||||
<div class="" aria-label="Edit controls">
|
|
||||||
<button type="button" class="btn btn-outline-warning btn-sm"><svg class="edit-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M100.4 417.2C104.5 402.6 112.2 389.3 123 378.5L304.2 197.3L338.1 163.4C354.7 180 389.4 214.7 442.1 267.4L476 301.3L442.1 335.2L260.9 516.4C250.2 527.1 236.8 534.9 222.2 539L94.4 574.6C86.1 576.9 77.1 574.6 71 568.4C64.9 562.2 62.6 553.3 64.9 545L100.4 417.2zM156 413.5C151.6 418.2 148.4 423.9 146.7 430.1L122.6 517L209.5 492.9C215.9 491.1 221.7 487.8 226.5 483.2L155.9 413.5zM510 267.4C493.4 250.8 458.7 216.1 406 163.4L372 129.5C398.5 103 413.4 88.1 416.9 84.6C430.4 71 448.8 63.4 468 63.4C487.2 63.4 505.6 71 519.1 84.6L554.8 120.3C568.4 133.9 576 152.3 576 171.4C576 190.5 568.4 209 554.8 222.5C551.3 226 536.4 240.9 509.9 267.4z"/></svg></button>
|
|
||||||
<button type="button" class="btn btn-outline-danger btn-sm"><svg class="delete-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M232.7 69.9L224 96L128 96C110.3 96 96 110.3 96 128C96 145.7 110.3 160 128 160L512 160C529.7 160 544 145.7 544 128C544 110.3 529.7 96 512 96L416 96L407.3 69.9C402.9 56.8 390.7 48 376.9 48L263.1 48C249.3 48 237.1 56.8 232.7 69.9zM512 208L128 208L149.1 531.1C150.7 556.4 171.7 576 197 576L443 576C468.3 576 489.3 556.4 490.9 531.1L512 208z"/></svg></button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-row mt-1">
|
|
||||||
<div class="p small text-secondary">{card.setName}</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-row mt-1">
|
|
||||||
<div class="h5">{card.productName}</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-row mt-1 justify-content-between align-items-baseline">
|
|
||||||
<div class={`inv-grid-trend small ${isGain ? "up" : "down"}`}>
|
|
||||||
<span class="inv-grid-arrow">{isGain ? "▲" : "▼"}</span>
|
|
||||||
<span class="h6 my-0">${market.toFixed(2)}</span>
|
|
||||||
</div>
|
|
||||||
<div class={`inv-grid-delta small ${isGain ? "up" : "down"}`}>
|
|
||||||
{isGain ? "+" : "-"}${Math.abs(diff).toFixed(2)}</br>{isGain ? "+" : "-"}{Math.abs(pct).toFixed(2)}%
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="tableView" style="display:none">
|
<!-- <div id="tableView" style="display:none">
|
||||||
<div class="inv-list-wrap">
|
<div class="inv-list-wrap">
|
||||||
<table class="table align-middle mb-0 inv-list-table">
|
<table class="table align-middle mb-0 inv-list-table">
|
||||||
<tbody id="inventoryRows">
|
<tbody id="inventoryRows">
|
||||||
@@ -544,12 +204,12 @@ const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-secondary small mt-2 ps-1" id="rowCount"></div>
|
<div class="text-secondary small mt-2 ps-1" id="rowCount"></div>
|
||||||
</div>
|
</div> -->
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="modal fade" id="newCatalogModal" tabindex="-1" aria-labelledby="newCatalogLabel" aria-modal="true" role="dialog">
|
<!-- <div class="modal fade" id="newCatalogModal" tabindex="-1" aria-labelledby="newCatalogLabel" aria-modal="true" role="dialog">
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="modal-content bg-dark text-light border border-secondary">
|
<div class="modal-content bg-dark text-light border border-secondary">
|
||||||
<div class="modal-header border-secondary">
|
<div class="modal-header border-secondary">
|
||||||
@@ -629,420 +289,6 @@ const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
<Footer slot="footer" />
|
<Footer slot="footer" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
.view-toggle-btn {
|
|
||||||
text-decoration: none;
|
|
||||||
color: var(--bs-secondary-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-toggle-btn.active {
|
|
||||||
color: var(--bs-body-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-toggle-btn.active svg,
|
|
||||||
.view-toggle-btn:hover svg {
|
|
||||||
color: var(--bs-body-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
#catalogList .list-group-item.active {
|
|
||||||
background-color: rgba(var(--bs-danger-rgb), .15) !important;
|
|
||||||
color: rgba(var(--bs-danger-rgb), 1) !important;
|
|
||||||
border-left: 2px solid var(--bs-danger) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#gridView {
|
|
||||||
row-gap: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-card {
|
|
||||||
position: relative;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-media {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-body {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 1rem;
|
|
||||||
padding: .85rem .15rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-main {
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-title {
|
|
||||||
font-size: 1.9rem;
|
|
||||||
line-height: 1.05;
|
|
||||||
font-weight: 500;
|
|
||||||
margin-bottom: .4rem;
|
|
||||||
color: var(--bs-emphasis-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: .2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-submeta {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: .35rem;
|
|
||||||
font-size: .9rem;
|
|
||||||
color: var(--bs-secondary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-price {
|
|
||||||
min-width: 112px;
|
|
||||||
text-align: right;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: #f3f3f3 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-trend,
|
|
||||||
.inv-list-price-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: .35rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1.1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-value,
|
|
||||||
.inv-list-price {
|
|
||||||
font-size: 1.15rem;
|
|
||||||
color: #111;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-trend.up .inv-grid-arrow,
|
|
||||||
.inv-list-price-line.up .inv-grid-arrow {
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-trend.down,
|
|
||||||
.inv-list-price-line.down {
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-delta,
|
|
||||||
.inv-list-delta {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-delta.up,
|
|
||||||
.inv-list-delta.up {
|
|
||||||
color: #2e7d32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-delta.down,
|
|
||||||
.inv-list-delta.down {
|
|
||||||
color: #c62828;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-qty,
|
|
||||||
.inv-list-qty {
|
|
||||||
margin-top: .35rem;
|
|
||||||
font-size: .9rem;
|
|
||||||
color: #1ea7a1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-cart,
|
|
||||||
.inv-list-cart {
|
|
||||||
position: absolute;
|
|
||||||
right: .35rem;
|
|
||||||
bottom: .1rem;
|
|
||||||
width: 2.15rem;
|
|
||||||
height: 2.15rem;
|
|
||||||
border-radius: 999px;
|
|
||||||
border: 1px solid #4cb7b3;
|
|
||||||
background: transparent;
|
|
||||||
color: #38a9a5;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
transition: .15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-cart:hover,
|
|
||||||
.inv-list-cart:hover {
|
|
||||||
background: rgba(76, 183, 179, .08);
|
|
||||||
color: #2a9a96;
|
|
||||||
}
|
|
||||||
|
|
||||||
#tableView {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-wrap {
|
|
||||||
border-radius: 0;
|
|
||||||
overflow: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-table {
|
|
||||||
--bs-table-bg: transparent;
|
|
||||||
--bs-table-hover-bg: transparent;
|
|
||||||
--bs-table-color: inherit;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-table tbody,
|
|
||||||
.inv-list-table tr,
|
|
||||||
.inv-list-table td {
|
|
||||||
border: 0 !important;
|
|
||||||
background: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-row + .inv-list-row .inv-list-cardcell {
|
|
||||||
border-top: 1px solid rgba(0, 0, 0, .08) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-cardcell {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-card {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1rem;
|
|
||||||
min-height: 116px;
|
|
||||||
padding: .8rem 4.5rem .8rem .35rem;
|
|
||||||
background: #f3f3f3;
|
|
||||||
border: 1px solid rgba(0, 0, 0, .08);
|
|
||||||
border-radius: .35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-thumb {
|
|
||||||
width: 70px;
|
|
||||||
flex: 0 0 70px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-thumb img {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
border-radius: .25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-info {
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-name {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #111827;
|
|
||||||
margin-bottom: .35rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
max-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-name:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-meta {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: .1rem;
|
|
||||||
font-size: .95rem;
|
|
||||||
color: #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-setlink {
|
|
||||||
color: #5b6f8f;
|
|
||||||
text-decoration: underline;
|
|
||||||
text-underline-offset: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-condition {
|
|
||||||
margin-top: .35rem;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: .35rem;
|
|
||||||
font-size: .95rem;
|
|
||||||
color: #2b7a78;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-right {
|
|
||||||
margin-left: auto;
|
|
||||||
min-width: 140px;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.inv-grid-body {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-price {
|
|
||||||
min-width: 0;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-grid-trend {
|
|
||||||
justify-content: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-card {
|
|
||||||
align-items: flex-start;
|
|
||||||
padding-right: 3.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inv-list-right {
|
|
||||||
min-width: 0;
|
|
||||||
padding-right: .25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
import * as bootstrap from "bootstrap";
|
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
|
||||||
document.querySelectorAll(".modal").forEach((el) => {
|
|
||||||
bootstrap.Modal.getOrCreateInstance(el);
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll("#catalogList [data-catalog]").forEach((item) => {
|
|
||||||
item.addEventListener("click", () => {
|
|
||||||
document.querySelectorAll("#catalogList [data-catalog]").forEach((i) => i.classList.remove("active"));
|
|
||||||
item.classList.add("active");
|
|
||||||
|
|
||||||
const catalog = item.dataset.catalog ?? "all";
|
|
||||||
|
|
||||||
document.querySelectorAll("#gridView .col").forEach((col) => {
|
|
||||||
col.style.display = catalog === "all" ? "" : "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
document.querySelectorAll("#inventoryRows tr").forEach((row) => {
|
|
||||||
row.style.display = catalog === "all" ? "" : "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
updateRowCount();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const gridView = document.getElementById("gridView");
|
|
||||||
const tableView = document.getElementById("tableView");
|
|
||||||
const btnGrid = document.getElementById("btnGrid");
|
|
||||||
const btnTable = document.getElementById("btnTable");
|
|
||||||
const rowCount = document.getElementById("rowCount");
|
|
||||||
const tbody = document.getElementById("inventoryRows");
|
|
||||||
|
|
||||||
function showGrid() {
|
|
||||||
gridView.style.display = "";
|
|
||||||
tableView.style.display = "none";
|
|
||||||
btnGrid.classList.add("active");
|
|
||||||
btnTable.classList.remove("active");
|
|
||||||
}
|
|
||||||
|
|
||||||
function showTable() {
|
|
||||||
gridView.style.display = "none";
|
|
||||||
tableView.style.display = "";
|
|
||||||
btnGrid.classList.remove("active");
|
|
||||||
btnTable.classList.add("active");
|
|
||||||
updateRowCount();
|
|
||||||
}
|
|
||||||
|
|
||||||
btnGrid?.addEventListener("click", showGrid);
|
|
||||||
btnTable?.addEventListener("click", showTable);
|
|
||||||
|
|
||||||
const searchInput = document.getElementById("inventorySearch");
|
|
||||||
const clearBtn = document.getElementById("clearSearch");
|
|
||||||
|
|
||||||
let searchTimer;
|
|
||||||
|
|
||||||
searchInput?.addEventListener("input", () => {
|
|
||||||
clearTimeout(searchTimer);
|
|
||||||
searchTimer = setTimeout(() => {
|
|
||||||
const term = searchInput.value.toLowerCase();
|
|
||||||
clearBtn.classList.toggle("d-none", !term);
|
|
||||||
|
|
||||||
tbody?.querySelectorAll("tr").forEach((row) => {
|
|
||||||
row.style.display = row.textContent.toLowerCase().includes(term) ? "" : "none";
|
|
||||||
});
|
|
||||||
|
|
||||||
updateRowCount();
|
|
||||||
}, 120);
|
|
||||||
});
|
|
||||||
|
|
||||||
clearBtn?.addEventListener("click", () => {
|
|
||||||
searchInput.value = "";
|
|
||||||
clearBtn.classList.add("d-none");
|
|
||||||
tbody?.querySelectorAll("tr").forEach((r) => {
|
|
||||||
r.style.display = "";
|
|
||||||
});
|
|
||||||
updateRowCount();
|
|
||||||
});
|
|
||||||
|
|
||||||
function updateRowCount() {
|
|
||||||
if (!rowCount || !tbody) return;
|
|
||||||
const visible = [...tbody.querySelectorAll("tr")].filter((r) => r.style.display !== "none").length;
|
|
||||||
rowCount.textContent = `Showing ${visible} card${visible !== 1 ? "s" : ""}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRowCount();
|
|
||||||
|
|
||||||
document.getElementById("createCatalogBtn")?.addEventListener("click", () => {
|
|
||||||
const input = document.getElementById("catalogNameInput");
|
|
||||||
const name = input.value.trim();
|
|
||||||
if (!name) {
|
|
||||||
input.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const li = document.createElement("li");
|
|
||||||
li.className =
|
|
||||||
"list-group-item list-group-item-action bg-transparent text-light border-0 rounded px-2 py-2 d-flex align-items-center gap-2";
|
|
||||||
li.setAttribute("data-catalog", name);
|
|
||||||
li.setAttribute("role", "button");
|
|
||||||
li.style.cursor = "pointer";
|
|
||||||
li.innerHTML = `<span class="text-secondary" style="font-size:.7rem">▸</span>${name}`;
|
|
||||||
|
|
||||||
li.addEventListener("click", () => {
|
|
||||||
document.querySelectorAll("#catalogList [data-catalog]").forEach((i) => i.classList.remove("active"));
|
|
||||||
li.classList.add("active");
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("catalogList")?.appendChild(li);
|
|
||||||
|
|
||||||
input.value = "";
|
|
||||||
//bootstrap.Modal.getInstance(document.getElementById("newCatalogModal"))?.hide();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("csvFileInput")?.addEventListener("change", (e) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (!file) return;
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (ev) => {
|
|
||||||
const lines = ev.target.result.split("\n").slice(0, 4);
|
|
||||||
const preview = document.getElementById("csvPreviewContent");
|
|
||||||
preview.textContent = lines.join("\n");
|
|
||||||
document.getElementById("csvPreview")?.classList.remove("d-none");
|
|
||||||
};
|
|
||||||
reader.readAsText(file);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
@@ -1,49 +1,186 @@
|
|||||||
---
|
---
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
import Layout from '../layouts/Main.astro';
|
import Layout from '../layouts/Main.astro';
|
||||||
import NavItems from '../components/NavItems.astro';
|
import Search from '../components/Search.astro';
|
||||||
import NavBar from '../components/NavBar.astro';
|
import NavBar from '../components/NavBar.astro';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
import { Show, SignInButton, SignUpButton, SignOutButton } from '@clerk/astro/components'
|
import { Show, SignInButton } from '@clerk/astro/components'
|
||||||
|
import Hero from '../components/Hero.astro';
|
||||||
|
import BackToTop from '../components/BackToTop.astro';
|
||||||
|
import NavItems from '../components/NavItems.astro';
|
||||||
---
|
---
|
||||||
<Layout title="Rigid's App Thing">
|
<Layout title="RAT - Realtime, Accurate and Transparent TCG Pricing Data" >
|
||||||
<NavBar slot="navbar">
|
<Hero slot="page" />
|
||||||
<NavItems slot="navItems" />
|
<div slot="page">
|
||||||
</NavBar>
|
<!-- ═══════════════════════════════════════════
|
||||||
<div class="row mb-4" slot="page">
|
SOCIAL PROOF / STATS BAR
|
||||||
<div class="col-12">
|
═══════════════════════════════════════════ -->
|
||||||
<h1>Rigid's App Thing</h1>
|
<section class="stats-bar py-4 border-top border-bottom border-subtle" aria-label="Platform statistics">
|
||||||
<p class="text-secondary">(working title)</p>
|
<div class="container">
|
||||||
|
<ul class="list-unstyled d-flex flex-wrap justify-content-center justify-content-md-between gap-4 mb-0 text-center">
|
||||||
|
<li>
|
||||||
|
<strong class="d-block fs-4 fw-bold text-orchid">Pokémon TCG</strong>
|
||||||
|
<span class="text-body-secondary small">All EN and JP Sets</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong class="d-block fs-4 fw-bold text-orchid">All Conditions</strong>
|
||||||
|
<span class="text-body-secondary small">NM · LP · MP · HP · DMG</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong class="d-block fs-4 fw-bold text-orchid">Real-Time</strong>
|
||||||
|
<span class="text-body-secondary small">Accurate Market Prices</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<strong class="d-block fs-4 fw-bold text-orchid">100% Free</strong>
|
||||||
|
<span class="text-body-secondary small">Pricing Features Always Free</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6 mb-2">
|
</section>
|
||||||
<h2 class="mt-3">Welcome!</h2>
|
<!-- ═══════════════════════════════════════════
|
||||||
<p class="mt-2">
|
CORE FEATURES
|
||||||
You've been selected to participate in the closed beta! This single page web application is meant to elevate condition/variant data for the Pokemon TCG. In future iterations, we will add vendor inventory/collection management features, as well.
|
═══════════════════════════════════════════ -->
|
||||||
</p>
|
<section class="py-6" aria-labelledby="features-heading">
|
||||||
<p class="my-2">
|
<div class="container">
|
||||||
After the closed beta is complete, the app will move into a more open beta. Feel free to play "Who's that Pokémon?" with the random Pokémon generator <a href="/404">here</a>. Refresh the page to see a new Pokémon!
|
<header class="text-center mb-5">
|
||||||
</p>
|
<h2 id="features-heading" class="h1 fw-bold">Everything you need to collect smarter</h2>
|
||||||
<Show when="signed-in">
|
<p class="text-body-secondary lead mt-2">Built by collectors, for collectors. No fluff.</p>
|
||||||
<a href="/pokemon" class="btn btn-warning mt-2">Take me to the cards</a>
|
</header>
|
||||||
</Show>
|
|
||||||
|
<div class="row g-4">
|
||||||
|
|
||||||
|
<article class="col-md-5 offset-md-1">
|
||||||
|
<div class="feature-card h-100 p-4 rounded-3">
|
||||||
|
<div class="feature-icon mb-3" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="currentColor"><path d="M61.1 80C65.6 80 69.9 80.9 73.8 82.6C94.9 83.1 114.1 87.8 133.4 89C200.4 91.5 253.4 119.6 315.5 154.1C395 101.5 477 91.8 567.9 91.8C575.4 91.8 582.3 94.4 587.7 98.7C599.8 103.9 607.9 116.3 607 130.2C608.8 244.9 610.7 359.8 607.1 480.4C607 499.1 590.9 513.5 572.4 512.2C492.7 511.5 409.1 518.2 343.4 564.9C337.4 572 328.1 576.5 318.1 576.2C278.5 566 227.1 523.4 183.2 514.2C146.8 502.7 108.4 497.3 59 497.3C41 497.7 25.4 481 27.1 463.1C32.7 382.2 31.5 301.8 30.2 219.9C29.6 184.4 29.1 148.6 29.1 112.3C29.1 94.6 43.4 80.3 61.1 80.3zM351.3 487C411.1 455.7 476.9 449.1 543.4 448.6C542.9 366.4 549.9 301.3 544.5 225.1C543.3 202.7 542.1 179.4 542.2 155.9C467.7 157.9 407.4 169.4 349.4 208.2L351 318.9C352.1 348.3 351.7 428.5 351.3 487zM285.4 210.6C222.2 172.7 168.9 152.1 99.5 149.6C98 149.4 94.8 148.9 93.3 148.7C94.1 243.5 97.5 338.5 92.9 433.9C165.6 437.5 224.2 455.5 287.2 489.7C287.6 430.8 288.1 349.6 286.9 321.1C286.9 319.9 285.3 212 285.3 210.6z"/></svg>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6 d-flex justify-content-end align-items-start mt-3">
|
<h3 class="h5 fw-semibold mb-2">Complete Card Database</h3>
|
||||||
<div class="d-flex gap-3">
|
<p class="text-body-secondary mb-0">Search across every English and Japanese set. Find any card instantly with the condition-by-condition pricing you need to buy, sell, or trade with confidence.</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="col-md-5">
|
||||||
|
<div class="feature-card h-100 p-4 rounded-3">
|
||||||
|
<div class="feature-icon mb-3" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="currentColor"><path d="M352 75.9C341.8 27.5 275.6 48.1 289.7 94.7C228.2 99.8 143.7 131.4 148 205.2C146.2 288.3 198 322.6 268.5 334.4C275.9 336.1 282.3 337.7 289.4 339.1C287.8 386.9 288.9 436.4 289.2 486.6C281 486 272.6 485.3 263.8 484.5C230.4 486.3 155 458.7 152.2 513.8C158.4 561.8 224.6 538.9 257.8 548.2C267.9 549.1 278.2 550 288.6 550.7C280.7 604.6 357.9 605.5 352.6 551.4C436.9 548.7 522.3 498.7 510.9 401.3C491.9 312.8 416.8 300.9 354.7 285.5C355.7 247.4 353.5 210.7 354.9 172.9C355 167.1 355.2 161.2 355.3 155.2C390.7 151.6 466.5 183.2 472.1 128.2C470.4 86.7 423.3 98.1 394.1 93.7C380.6 92.7 367.6 91.5 353.6 91.1C353.2 86.1 352.6 81.1 352 76.1zM291.2 159.1C291.1 162.7 291 166.3 291 169.9C289.7 203.1 291.5 240.2 291 273.8C287.7 273 284.3 272.2 280.9 271.5C266.1 268.8 226.6 257.8 221.8 247.9C216.1 237.1 211.8 221.1 212.1 206.6C212.4 191.9 217 184.6 221.9 181.4C239.9 169.3 264.3 163 291.3 159.1zM353.2 350.2C395.6 359.3 446.4 378.1 447.9 411.8C452.7 424.7 433.7 465.1 421.1 468.7C398.1 479.6 376 484.9 353.3 487C353.2 469.7 353.1 452.1 352.9 436.4C352.5 402.6 352.4 382 353.3 350.2z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="h5 fw-semibold mb-2">Condition-Graded Pricing</h3>
|
||||||
|
<p class="text-body-secondary mb-0">NM, LP, MP, HP, and DMG prices displayed side by side. Stop guessing what a played card is worth — see every tier at once so you never undersell or overpay.</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="col-md-5 offset-md-1">
|
||||||
|
<div class="feature-card h-100 p-4 rounded-3">
|
||||||
|
<div class="feature-icon mb-3" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="currentColor"><path d="M131.3 233.2C168.5 232.6 164.2 190.9 162.6 164.5C190.2 164.1 230.5 171.2 232.2 132.3C230.2 92.4 188.2 101 160.1 100.5C159.4 73.8 164.9 32.3 127.4 31.6C89.7 32.2 94.7 74.8 96.1 101.4C84.7 101.4 73.4 102.9 62 103.7C44.4 104.9 31.1 120.2 32.3 137.9C33.5 155.6 48.9 168.8 66.5 167.6L97.5 165.4L98.6 165.4C99.1 191.9 94.2 232.7 131.3 233.3zM351.7 89.4C346.2 56.4 295.9 55.4 289.2 88.2C274.7 149.7 261.2 189.6 226.5 236.8L162.1 274.8C154.2 278 146 281.3 137.7 284.7C119.7 292.1 101.3 299.6 85.2 305.6C57.3 314.9 58 357.6 86.2 366C123.2 378.2 185.4 393.6 225.9 403.2L275.1 508.8L284.9 541C292.3 568.9 334.2 571.4 344.8 544.5C366.2 495.4 391.6 447.3 414 398C421.1 395.3 427.8 392.7 434.3 390.2C465.2 377.4 499.8 367.3 533.2 358.9C560.9 352.5 565.3 311.7 539.6 299.5C530.9 295.1 522.2 290.5 513.3 285.9L513.3 285.9C480.7 268.9 446.8 251.2 412 236L369.3 162.7C363.7 146 355.7 109.7 351.7 89.5L351.7 89.4L351.7 89.4zM318.2 449.9L277.3 362.1C273.1 353.2 265.1 346.6 255.5 344.4L255.2 344.4L254.3 344.2C235.6 339.9 217.8 335.5 194.9 329.8L264.5 288.7C290.5 265.4 306.2 230.8 319.4 204.3L362 277.2C380.9 299.8 414.9 303.7 439.5 319.7C428.6 323.6 418 327.7 407.7 331.7L407.7 331.7C391.2 339.5 369.1 341 361.1 359.5C347.8 389.5 332.9 419.7 318.3 449.8zM579.4 551.9C621.3 551.7 621.2 488.1 579.4 487.9L546.3 487.9C545.8 458.8 550.9 428.4 516.7 424C480.7 422.1 479.7 460.5 482.3 487.9L451.4 487.9C433.7 487.9 419.4 502.2 419.4 519.9C419.4 537.6 433.7 551.9 451.4 551.9L482.7 551.9C480.9 580.3 479.1 614.5 514.2 615.9C549.5 614.4 546.9 580.5 546.7 551.9L579.3 551.9z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="h5 fw-semibold mb-2">All Variants</h3>
|
||||||
|
<p class="text-body-secondary mb-0">We display every card variant separately—no stacking—so you can see true edition-level prices, trends, and rarity at a glance..</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="col-md-5">
|
||||||
|
<div class="feature-card h-100 p-4 rounded-3">
|
||||||
|
<div class="feature-icon mb-3" aria-hidden="true">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" fill="currentColor"><path d="M282.5 63.6C42.4 80.2-19.5 451.3 198.9 550.3C304.7 599.7 469.7 580.2 540.7 463.3C568.1 414.5 576.4 348.1 576.4 298.4C570.5 220.7 504.8 130 442.3 92.4C392.2 63.7 333.8 64.3 282.5 63.6zM182.6 175.9C206.7 151.4 247.1 133.5 283.8 127.6C336.9 128.2 388.8 127.6 429.6 160.2C456.6 183.9 513.6 262.4 512.4 298.4C512.4 343.9 504.4 397.3 484.9 431.9C440.5 507.9 311.1 532.7 226.4 492.4C106.4 438.8 101.7 266.4 182.6 175.9zM353.7 207.9C354.1 166.1 290.4 165.2 289.7 207.1L288.4 315.8C290.9 362.7 346.7 343.6 378.7 349.6C404.9 352.1 446.9 357.6 448.4 320.3C446 272.8 385.2 290 352.8 284.1L353.8 207.9L353.8 207.9z"/></svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="h5 fw-semibold mb-2">Fast Search, Instant Results</h3>
|
||||||
|
<p class="text-body-secondary mb-0">Type a card name + number or search by eras like "e-reader" or "SWSH". Powerful filters let you drill into exactly the set, variant, rarity or card type you care about.</p>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════
|
||||||
|
UPCOMING PREMIUM / CTA TEASER
|
||||||
|
═══════════════════════════════════════════ -->
|
||||||
|
<section class="premium-section py-6" aria-labelledby="premium-heading">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row align-items-center g-4">
|
||||||
|
|
||||||
|
<div class="col-lg-5">
|
||||||
|
<p class="eyebrow text-lilac mb-3">Coming Soon · Premium</p>
|
||||||
|
<h2 id="premium-heading" class="h1 fw-bold mb-3">
|
||||||
|
<span class="text-gradient">Your collection,<br/>fully managed.</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-body-secondary lead mb-4">
|
||||||
|
We're building a suite of inventory and curation tools for serious collectors. Sign up free today and be first in line when they launch.
|
||||||
|
</p>
|
||||||
<Show when="signed-out">
|
<Show when="signed-out">
|
||||||
<SignInButton asChild mode="modal">
|
<SignInButton asChild mode="modal">
|
||||||
<button class="btn btn-success">Sign In</button>
|
<button class="btn btn-purple btn-lg px-4">
|
||||||
|
Join Now! — It's Free
|
||||||
|
</button>
|
||||||
</SignInButton>
|
</SignInButton>
|
||||||
<SignUpButton asChild mode="modal">
|
|
||||||
<button class="btn btn-dark">Request Access</button>
|
|
||||||
</SignUpButton>
|
|
||||||
</Show>
|
|
||||||
<Show when="signed-in">
|
|
||||||
<SignOutButton asChild>
|
|
||||||
<button class="btn btn-danger">Sign Out</button>
|
|
||||||
</SignOutButton>
|
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-7">
|
||||||
|
<div class="premium-list">
|
||||||
|
|
||||||
|
<div class="premium-item p-4 rounded-3 mb-3">
|
||||||
|
<div class="d-flex align-items-start gap-3">
|
||||||
|
<span class="badge-coming">Coming Soon</span>
|
||||||
|
<div>
|
||||||
|
<h3 class="h6 fw-semibold mb-1">Collection Portfolio Tracker</h3>
|
||||||
|
<p class="text-body-secondary small mb-0">Add cards you own with their condition and purchase price. Watch your total collection value update in real time as market prices shift — so you always know your net position.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="premium-item p-4 rounded-3 mb-3">
|
||||||
|
<div class="d-flex align-items-start gap-3">
|
||||||
|
<span class="badge-coming">Coming Soon</span>
|
||||||
|
<div>
|
||||||
|
<h3 class="h6 fw-semibold mb-1">Latest Sales Aggregation</h3>
|
||||||
|
<p class="text-body-secondary small mb-0">See recent sale prices across different trusted marketplaces, including TCGPlayer and eBay. Make informed purchases based on real-time market activity, not just Market Price.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="premium-item p-4 rounded-3">
|
||||||
|
<div class="d-flex align-items-start gap-3">
|
||||||
|
<span class="badge-coming">Coming Soon</span>
|
||||||
|
<div>
|
||||||
|
<h3 class="h6 fw-semibold mb-1">Graded Card Inventory</h3>
|
||||||
|
<p class="text-body-secondary small mb-0">Log PSA, BGS, and CGC slabs with their cert numbers, grades, and current values. Track your graded portfolio separately from your raw collection.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ═══════════════════════════════════════════
|
||||||
|
FINAL CTA
|
||||||
|
═══════════════════════════════════════════ -->
|
||||||
|
<section class="cta-section py-6" aria-labelledby="cta-heading">
|
||||||
|
<div class="container text-center">
|
||||||
|
<h2 id="cta-heading" class="display-5 fw-bold mb-3">Ready to join the RAT Pack?</h2>
|
||||||
|
<p class="lead text-body-secondary mb-4 mx-auto" style="max-width: 520px;">
|
||||||
|
Join free today. Browse every card, track prices across conditions, and get early access to premium collection tools as we build them.
|
||||||
|
</p>
|
||||||
|
<div class="d-flex flex-wrap justify-content-center gap-3">
|
||||||
|
<Show when="signed-out">
|
||||||
|
<SignInButton asChild mode="modal">
|
||||||
|
<button class="btn btn-purple btn-lg px-5">
|
||||||
|
Create Free Account
|
||||||
|
</button>
|
||||||
|
</SignInButton>
|
||||||
|
</Show>
|
||||||
|
<a href="/pokemon" class="btn btn-outline-light btn-lg px-5">Browse Cards First</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<BackToTop />
|
||||||
|
</div>
|
||||||
|
|
||||||
<Footer slot="footer" />
|
<Footer slot="footer" />
|
||||||
</Layout>
|
</Layout>
|
||||||
@@ -10,17 +10,8 @@ import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
|
|||||||
|
|
||||||
import { Tooltip } from "bootstrap";
|
import { Tooltip } from "bootstrap";
|
||||||
|
|
||||||
import { clerkClient } from '@clerk/astro/server';
|
// auth check for inventory management features
|
||||||
|
const { canAddInventory } = Astro.locals;
|
||||||
const { userId, has } = Astro.locals.auth();
|
|
||||||
const TARGET_ORG_ID = 'org_3Baav9czkRLLlC7g89oJWqRRulK';
|
|
||||||
|
|
||||||
let hasAccess = has({ feature: 'inventory_add' });
|
|
||||||
|
|
||||||
if (!hasAccess && userId) {
|
|
||||||
const memberships = await clerkClient(Astro).users.getOrganizationMembershipList({ userId });
|
|
||||||
hasAccess = memberships.data.some(m => m.organization.id === TARGET_ORG_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const partial = true;
|
export const partial = true;
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
@@ -239,8 +230,8 @@ const altSearchUrl = (card: any) => {
|
|||||||
<span class="rarity-icon-large position-absolute bottom-0 end-0 d-inline"><RarityIcon rarity={card?.rarityName} /></span>
|
<span class="rarity-icon-large position-absolute bottom-0 end-0 d-inline"><RarityIcon rarity={card?.rarityName} /></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column flex-lg-row justify-content-between mt-2">
|
<div class="d-flex flex-column flex-lg-row justify-content-between mt-2">
|
||||||
<div class="text-secondary">{card?.set?.setCode}</div>
|
<div class="text-secondary"><span class="d-flex d-xxl-none">{card?.set?.setCode}</span><span class="d-none d-xxl-flex">{card?.set?.setName}</span></div>
|
||||||
<div class="text-secondary">Illus<span class="d-none d-lg-inline">trator</span>: {card?.artist}</div>
|
<div class="text-secondary">Illus<span class="d-none d-xxl-inline">trator</span>: {card?.artist}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -272,7 +263,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
<span class="d-none">Damaged</span><span class="d-inline">DMG</span>
|
<span class="d-none">Damaged</span><span class="d-inline">DMG</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
{hasAccess && (
|
{canAddInventory && (
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link vendor" id="vendor-tab" data-bs-toggle="tab" data-bs-target="#nav-vendor" type="button" role="tab" aria-controls="nav-vendor" aria-selected="false">
|
<button class="nav-link vendor" id="vendor-tab" data-bs-toggle="tab" data-bs-target="#nav-vendor" type="button" role="tab" aria-controls="nav-vendor" aria-selected="false">
|
||||||
<span class="d-none d-xxl-inline">Inventory</span><span class="d-xxl-none">+/-</span>
|
<span class="d-none d-xxl-inline">Inventory</span><span class="d-xxl-none">+/-</span>
|
||||||
@@ -289,7 +280,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
<div class="d-flex flex-column gap-1">
|
<div class="d-flex flex-column gap-1">
|
||||||
|
|
||||||
<!-- Stat cards -->
|
<!-- Stat cards -->
|
||||||
<div class="d-flex flex-fill flex-row gap-1">
|
<div class="d-flex flex-fill flex-row gap-1 flex-wrap flex-lg-nowrap">
|
||||||
<div class="alert alert-dark rounded p-2 flex-fill d-flex flex-column mb-0">
|
<div class="alert alert-dark rounded p-2 flex-fill d-flex flex-column mb-0">
|
||||||
<h6 class="mb-auto">Market Price</h6>
|
<h6 class="mb-auto">Market Price</h6>
|
||||||
<p class="mb-0 mt-1">${price.marketPrice}</p>
|
<p class="mb-0 mt-1">${price.marketPrice}</p>
|
||||||
@@ -361,15 +352,15 @@ const altSearchUrl = (card: any) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{hasAccess && (
|
{canAddInventory && (
|
||||||
<div class="tab-pane fade" id="nav-vendor" role="tabpanel" aria-labelledby="nav-vendor" tabindex="0">
|
<div class="tab-pane fade" id="nav-vendor" role="tabpanel" aria-labelledby="nav-vendor" tabindex="0">
|
||||||
<div class="row g-4">
|
<div class="row g-5">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<h6 class="mt-1 mb-2">Add {card?.productName} to inventory</h6>
|
<h6 class="mt-1 mb-2">Add {card?.productName} to inventory</h6>
|
||||||
|
|
||||||
<form id="inventoryForm" data-inventory-form novalidate>
|
<form id="inventoryForm" data-inventory-form novalidate>
|
||||||
<div class="row gx-3 gy-1">
|
<div class="row gx-3 gy-1">
|
||||||
<div class="col-3">
|
<div class="col-12 col-lg-3">
|
||||||
<label for="quantity" class="form-label">Quantity</label>
|
<label for="quantity" class="form-label">Quantity</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
@@ -384,7 +375,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
<div class="invalid-feedback">Required.</div>
|
<div class="invalid-feedback">Required.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-9">
|
<div class="col-12 col-lg-9">
|
||||||
<div class="d-flex justify-content-between align-items-start gap-2 flex-wrap">
|
<div class="d-flex justify-content-between align-items-start gap-2 flex-wrap">
|
||||||
<label for="purchasePrice" class="form-label">
|
<label for="purchasePrice" class="form-label">
|
||||||
Purchase price
|
Purchase price
|
||||||
@@ -436,7 +427,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label">Condition</label>
|
<label class="form-label">Condition</label>
|
||||||
<div class="btn-group condition-input w-100" role="group" aria-label="Condition">
|
<div class="btn-group btn-group-sm condition-input w-100 col-12" role="group" aria-label="Condition">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
class="btn-check"
|
class="btn-check"
|
||||||
@@ -521,7 +512,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
class="form-control"
|
class="form-control"
|
||||||
id="note"
|
id="note"
|
||||||
name="note"
|
name="note"
|
||||||
rows="2"
|
rows="3"
|
||||||
maxlength="255"
|
maxlength="255"
|
||||||
placeholder="e.g. bought at local shop, gift, graded copy…"
|
placeholder="e.g. bought at local shop, gift, graded copy…"
|
||||||
></textarea>
|
></textarea>
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
---
|
---
|
||||||
import { client } from '../../db/typesense';
|
import { client } from '../../db/typesense';
|
||||||
import { clerkClient } from '@clerk/astro/server';
|
|
||||||
|
|
||||||
const { userId, has } = Astro.locals.auth();
|
|
||||||
const TARGET_ORG_ID = 'org_3Baav9czkRLLlC7g89oJWqRRulK';
|
|
||||||
|
|
||||||
let hasAccess = has({ feature: 'inventory_add' });
|
|
||||||
|
|
||||||
if (!hasAccess && userId) {
|
|
||||||
const memberships = await clerkClient(Astro).users.getOrganizationMembershipList({ userId });
|
|
||||||
hasAccess = memberships.data.some(m => m.organization.id === TARGET_ORG_ID);
|
|
||||||
}
|
|
||||||
|
|
||||||
import RarityIcon from '../../components/RarityIcon.astro';
|
import RarityIcon from '../../components/RarityIcon.astro';
|
||||||
import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
|
import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
import * as util from 'util';
|
import * as util from 'util';
|
||||||
|
|
||||||
|
// auth check for inventory management features
|
||||||
|
const { canAddInventory } = Astro.locals;
|
||||||
|
|
||||||
// all the facet fields we want to use for filtering
|
// all the facet fields we want to use for filtering
|
||||||
const facetFields:any = {
|
const facetFields:any = {
|
||||||
@@ -102,7 +92,7 @@ const facetFilter = (facet:string) => {
|
|||||||
// primary search values (for cards)
|
// primary search values (for cards)
|
||||||
let searchArray = [{
|
let searchArray = [{
|
||||||
collection: 'cards',
|
collection: 'cards',
|
||||||
filter_by: `sealed:false${languageFilter}${queryFilter ? ` && ${queryFilter}` : ''}${filterBy ? ` && ${filterBy}` : ''}`,
|
filter_by: `$skus(id:*) && sealed:false${languageFilter}${queryFilter ? ` && ${queryFilter}` : ''}${filterBy ? ` && ${filterBy}` : ''}`,
|
||||||
per_page: 20,
|
per_page: 20,
|
||||||
facet_by: '',
|
facet_by: '',
|
||||||
max_facet_values: 0,
|
max_facet_values: 0,
|
||||||
@@ -143,6 +133,7 @@ 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 "–"
|
// 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 formatPrice = (condition:string, skus: any) => {
|
||||||
|
if (typeof skus === 'undefined' || skus.length === 0) return '—';
|
||||||
const sku:any = skus.find((price:any) => price.condition === condition);
|
const sku:any = skus.find((price:any) => price.condition === condition);
|
||||||
if (typeof sku === 'undefined' || typeof sku.marketPrice === 'undefined') return '—';
|
if (typeof sku === 'undefined' || typeof sku.marketPrice === 'undefined') return '—';
|
||||||
|
|
||||||
@@ -191,7 +182,7 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
<button type="button" data-bs-dismiss="offcanvas" class="btn btn-danger me-2" id="clear-filters">Clear</button>
|
<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>
|
<button type="submit" form="searchform" data-bs-dismiss="offcanvas" class="btn btn-success">Apply Filters</button>
|
||||||
</div>
|
</div>
|
||||||
{facets.map((facet) => (
|
{facets.map((facet: any) => (
|
||||||
<div class="mt-2 mb-4 facet-group row align-items-center justify-content-between">
|
<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>
|
<div class="fs-5 m-0 col-auto pb-1 border-bottom border-light-subtle">{facetNames(facet.field_name)}</div>
|
||||||
{(facet.counts.length > 20) &&
|
{(facet.counts.length > 20) &&
|
||||||
@@ -212,7 +203,7 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
</div>
|
</div>
|
||||||
<div id="sortBy" class="d-flex flex-fill align-items-center me-auto gap-2" hx-swap-oob="true">
|
<div id="sortBy" class="d-flex flex-fill align-items-center me-auto gap-2" hx-swap-oob="true">
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-sm btn-dark dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Sort by</button>
|
<button class="btn btn-sm btn-dark dropdown-toggle" data-toggle="sort-dropdown" aria-expanded="false">Sort by</button>
|
||||||
<ul class="dropdown-menu dropdown-menu-dark">
|
<ul class="dropdown-menu dropdown-menu-dark">
|
||||||
<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: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="releaseDate:asc,number:asc" data-label="Set: Oldest to Newest">Set: Oldest to Newest</a></li>
|
||||||
@@ -224,9 +215,9 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
</div>
|
</div>
|
||||||
<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>
|
<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">
|
<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-outline-secondary 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-outline-secondary 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>
|
<button type="button" class={`btn btn-outline-secondary language-btn${language === 'jp' ? ' active' : ''}`} data-lang="jp">JP</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="totalResults" class="d-flex text-secondary small d-none align-items-center" hx-swap-oob="true">
|
<div id="totalResults" class="d-flex text-secondary small d-none align-items-center" hx-swap-oob="true">
|
||||||
@@ -295,9 +286,9 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
|
|
||||||
{pokemon.map((card:any) => (
|
{pokemon.map((card:any) => (
|
||||||
<div class="col">
|
<div class="col">
|
||||||
{hasAccess && (
|
{canAddInventory && (
|
||||||
<button type="button" class="btn btn-sm inventory-button position-relative float-end shadow-filter text-center p-2" 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="event.stopPropagation(); sessionStorage.setItem('openModalTab', 'nav-vendor');">
|
<button type="button" class="btn btn-sm inventory-button position-relative float-end shadow-filter text-center p-2 fw-bold" 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="event.stopPropagation(); sessionStorage.setItem('openModalTab', 'nav-vendor');">
|
||||||
<b>+/–</b>
|
+/–
|
||||||
</button>
|
</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="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'});">
|
||||||
@@ -314,13 +305,13 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div class="h5 my-0">{card.productName}</div>
|
<div class="fs-5 fw-semibold my-0">{card.productName}</div>
|
||||||
<div class="d-flex flex-row lh-1 mt-1 justify-content-between">
|
<div class="d-flex flex-row lh-1 mt-1 justify-content-between">
|
||||||
<div class="text-secondary flex-grow-1 d-none d-lg-flex">{card.setName}</div>
|
<div class="text-body-tertiary flex-grow-1 d-none d-lg-flex fst-normal">{card.setName}</div>
|
||||||
<div class="text-body-tertiary">{card.number}</div>
|
<div class="text-body-tertiary fst-normal">{card.number}</div>
|
||||||
<span class="ps-2 small-icon"><RarityIcon rarity={card.rarityName} /></span>
|
<span class="ps-2 small-icon"><RarityIcon rarity={card.rarityName} /></span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-body-tertiary">{card.variant}</div><span class="d-none">{card.productId}</span>
|
<div class="text-secondary fst-italic">{card.variant}</div><span class="d-none">{card.productId}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
))}
|
))}
|
||||||
|
|||||||
145
src/pages/partials/inventory-cards.astro
Normal file
145
src/pages/partials/inventory-cards.astro
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
---
|
||||||
|
import { client } from '../../db/typesense';
|
||||||
|
import RarityIcon from '../../components/RarityIcon.astro';
|
||||||
|
import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
|
||||||
|
export const prerender = false;
|
||||||
|
|
||||||
|
import * as util from 'util';
|
||||||
|
import { db } from '../../db';
|
||||||
|
|
||||||
|
// 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 { userId } = Astro.locals.auth();
|
||||||
|
|
||||||
|
const InventoryDetails = async (inventoryId: string) => {
|
||||||
|
//console.log('inventoryid', inventoryId);
|
||||||
|
const details = await db.query.inventory.findFirst({
|
||||||
|
where: { inventoryId: inventoryId },
|
||||||
|
with: { sku: { with: { card: true } } }
|
||||||
|
})
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// primary search values (for cards)
|
||||||
|
let searchArray = [{
|
||||||
|
collection: 'inventories',
|
||||||
|
filter_by: `userId:=${userId}`,
|
||||||
|
per_page: 20,
|
||||||
|
facet_by: '',
|
||||||
|
max_facet_values: 0,
|
||||||
|
page: Math.floor(start / 20) + 1,
|
||||||
|
include_fields: 'id',
|
||||||
|
}];
|
||||||
|
|
||||||
|
// 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: 'content,setName,productLineName,rarityName,energyType,cardType'
|
||||||
|
// query_by: 'userId',
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 inventoryResults = searchResults.results[0] as any;
|
||||||
|
// console.log('inventoryResults', util.inspect(inventoryResults, { depth: null }));
|
||||||
|
|
||||||
|
|
||||||
|
const pokemon = inventoryResults.hits ?
|
||||||
|
await Promise.all(
|
||||||
|
inventoryResults.hits.map(
|
||||||
|
async (hit: any) => { return (await InventoryDetails(hit.document.id)); }
|
||||||
|
)
|
||||||
|
) : [];
|
||||||
|
const totalHits = inventoryResults?.found;
|
||||||
|
|
||||||
|
// console.log('pokemon', util.inspect(pokemon, { depth: null }));
|
||||||
|
console.log(`totalHits: ${totalHits}`);
|
||||||
|
---
|
||||||
|
|
||||||
|
{pokemon.map((inventory:any) => {
|
||||||
|
const sku = inventory.sku;
|
||||||
|
const card = sku.card;
|
||||||
|
const market = Number(sku.marketPrice) || 0;
|
||||||
|
const purchase = Number(inventory.purchasePrice) || 0;
|
||||||
|
const diff = market - purchase;
|
||||||
|
const pct = purchase > 0 ? (diff / purchase) * 100 : 0;
|
||||||
|
const isGain = diff >= 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class="col equal-height-col">
|
||||||
|
<div class="card-trigger position-relative inv-grid-media" data-card-id={card.productId} data-energy={card.energyType} data-rarity={card.rarityName} data-variant={card.variant} data-name={card.productName} data-bs-toggle="modal" data-bs-target="#cardModal">
|
||||||
|
<div class="rounded-4 card-image h-100">
|
||||||
|
<img src={`static/cards/${card.productId}.jpg`} alt={card.productName} loading="lazy" decoding="async" class="img-fluid rounded-4 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" style="z-index:4">
|
||||||
|
<FirstEditionIcon edition={card.variant} />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row justify-content-between my-1 align-items-center edit-bar">
|
||||||
|
<input type="number" class="form-control form-control-sm text-center" style="max-width: 33%;" value={inventory.quantity} min="1" max="999" aria-label="Quantity input" aria-describedby="button-minus button-plus">
|
||||||
|
<div class="" aria-label="Edit controls">
|
||||||
|
<button type="button" class="btn btn-sm btn-edit me-2"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" class="edit-svg"><path d="M374.4 146.5L374.4 146.5C395.3 128.4 443.5 85.4 468.5 97.6C484.4 105.9 497.8 112.8 501.3 120.1L501.4 120.2L501.5 120.3L501.6 120.5C507.6 130.2 518.4 150.1 519.6 161.6C513.5 192.1 484 217.1 461.6 240.4C425.7 208.6 404.1 187.1 369.7 150.6C371.3 149.2 372.8 147.9 374.4 146.5zM484.1 307.3C484.3 307.1 484.5 307 484.6 306.8C525.1 265.7 579.4 226.1 583.7 161.7C575.2 64.5 475.6-2.1 395.9 50.5C299.2 114.8 235.3 194.9 152 272.8C118.1 305.6 72.6 334.1 62.3 384.2C60.7 400.9 62.5 429.9 63.3 446.8C63.3 472.2 62.6 516.6 62.3 535.2C65.6 607.8 181.9 562 225.7 560.1C249.2 555.4 267.7 533 280.2 519.6C280.4 519.4 280.5 519.3 280.5 519.3C350.4 450.8 416.9 378.7 484.2 307.4zM416.2 285.7C349.4 358.2 282.1 428.8 211.2 497.7C182.1 502.9 154.5 507.3 126.6 510C128.2 471.4 125.7 426.3 125.8 391.4C138.8 367.7 172.1 340.8 194.2 320.8C239.9 279.4 276.7 234.2 323.2 194.7C356.5 230 380.6 254.2 416.1 285.7z"/></svg></button>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-sm btn-delete"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" class="delete-svg">
|
||||||
|
<filter id='shadow' color-interpolation-filters="sRGB">
|
||||||
|
<feDropShadow dx="2" dy="2" stdDeviation="2" flood-opacity="0.5"/>
|
||||||
|
</filter>
|
||||||
|
<path d="M345.1 124.8C361 125.2 374.2 126 386.3 127.8C386.2 137.1 385.4 148.3 384.6 157.9C340.9 156.9 297.8 157.6 254.5 158C254.8 149.7 252.4 136.7 255.2 129.7C279.3 123.4 313.6 125.1 345.1 124.7zM448.7 160.7C460.3 66.9 431.8 64.5 346.7 60.8C268.3 59.6 178 54.7 190.5 158.2C157.2 162.7 96.1 137.9 89.6 186.5C88.1 210.7 111.2 222.9 132.8 220.8L132.8 478.6C132.6 530.6 176.7 571.9 228.6 569.3L398.9 569.3C500.4 574.1 515.3 476.9 509.7 395.2C509.1 364.1 511.4 273.6 512.5 223.8C554.7 223.7 554.5 159.5 512.2 159.8L448.6 160.7zM448.5 224.7C446.1 301.9 445.5 378 445.8 440.3C442.6 476.1 442.9 507.5 400.1 505.2L227 505.2C211.1 506.8 196.6 494.9 196.7 478.5L196.7 222.1C280.4 222.3 366.5 219.1 448.4 224.6z"/></svg></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row mt-1">
|
||||||
|
<div class="text-body-tertiary flex-grow-1 small">{card.setName}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row my-1 justify-content-between mt-1">
|
||||||
|
<div class="fs-6 fw-semibold my-0">{card.productName}</div>
|
||||||
|
<div class="small my-0">{card.number}</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row justify-content-between align-items-baseline">
|
||||||
|
<div class="text-body-secondary">
|
||||||
|
<span class="small">Purchase</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-body-secondary">
|
||||||
|
<span class="small">Market</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-body-secondary">
|
||||||
|
<span class="small">Gain/Loss</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row justify-content-between align-items-baseline">
|
||||||
|
<div class="text-body-secondary">
|
||||||
|
<span class="h6 mt-0">${purchase.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-white">
|
||||||
|
<span class="h6 mt-0">${market.toFixed(2)}</span>
|
||||||
|
</div>
|
||||||
|
<div class={`h6 mt-0 ${isGain ? "text-success" : "text-danger"}`}>
|
||||||
|
{isGain ? "+" : "-"}${Math.abs(diff).toFixed(2)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
})
|
||||||
@@ -1,14 +1,9 @@
|
|||||||
---
|
---
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
import Layout from '../layouts/Main.astro';
|
import Layout from '../layouts/Main.astro';
|
||||||
import Search from '../components/Search.astro';
|
|
||||||
import CardGrid from "../components/CardGrid.astro";
|
import CardGrid from "../components/CardGrid.astro";
|
||||||
import NavBar from '../components/NavBar.astro';
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="Card Search">
|
<Layout title="Card Search">
|
||||||
<NavBar slot="navbar">
|
<CardGrid slot="page"/>
|
||||||
<Search slot="searchInput" />
|
|
||||||
</NavBar>
|
|
||||||
<CardGrid slot="page" />
|
|
||||||
</Layout>
|
</Layout>
|
||||||
24
src/pages/privacy.astro
Normal file
24
src/pages/privacy.astro
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
export const prerender = false;
|
||||||
|
import Footer from '../components/Footer.astro';
|
||||||
|
import Layout from '../layouts/Main.astro';
|
||||||
|
---
|
||||||
|
<Layout title="Terms and Privacy" >
|
||||||
|
<div class="container-fluid container-sm my-5" slot="page">
|
||||||
|
<section class="legal-info p-6 bg-gray-50 text-gray-800 rounded-md space-y-3">
|
||||||
|
<h2 class="text-xl font-semibold">Privacy & Terms</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
By signing in with your Google account, you agree to the following Terms of Service and Privacy Policy:
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Terms of Service:</strong> You may use our services only as permitted by law. Your account is for personal use and must be kept secure. We reserve the right to modify or discontinue services at any time. Misuse of the service may result in account suspension.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<strong>Privacy Policy:</strong> We use Clerk.js to handle authentication, ensuring that your Google account information is securely processed. We do not store your Google password. Personal data is only used to provide and improve our services. We do not sell your information to third parties. You have the right to access, correct, or delete your data by contacting us.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Footer slot="footer" />
|
||||||
|
</Layout>
|
||||||
1
src/svg/logo/rat_light.svg
Normal file
1
src/svg/logo/rat_light.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?><svg id="uuid-28885955-490e-4e5c-bba8-d29f831e6862" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 984.04107 353.35931"><g id="uuid-83941b04-85a7-4465-b9e0-012c209dc6a7"><path d="M653.62926,104.05915v32.23634h89.02801v217.06382h152.81484v-217.06382h88.56896V0h-330.4118v104.05915" style="fill:#fcf5f3;"/><g id="uuid-43578a3f-54c5-443e-9235-237fee6994bd"><path d="M262.03609,194.11787c12.84855-3.67235,23.01598-8.79185,30.51628-15.37407,7.49408-6.57599,13.07885-14.06852,16.7512-22.48691,3.67079-8.41061,6.03603-16.90058,7.11284-25.46836,1.06747-8.56155,1.60587-16.5209,1.60587-23.86404,0-25.99898-4.89698-46.80844-14.68473-62.41126-9.79552-15.60281-22.87438-26.91707-39.23656-33.95833C247.73105,3.51985,229.75366,0,210.17973,0H0v224.43011c2.52455-6.88838,5.14382-13.5023,7.8372-19.64065,18.36155-41.84105,49.28436-78.81895,78.08584-103.94867,28.79857-25.12952,75.6109-42.0873,99.07464-43.44673,23.46355-1.35962,37.54529,13.10025,43.17616,23.29335,5.63106,10.19291,4.89232,45.84328-18.69786,69.97672-23.59309,24.13363-53.42645,29.7079-70.68143,25.4174-17.25206-4.2907-23.29685-19.71592-21.08936-33.74282,2.20749-14.02709,13.38326-30.67734,26.43002-37.99014,13.04968-7.3126,24.90662-7.22157,30.40541-.71424,5.49588,6.50714-3.95517,14.38071-3.10146,16.03482.8537,1.6545,8.51098-4.88337,8.01945-13.10783-.49425-8.22447-11.28761-17.50765-32.61563-8.89591-21.33094,8.61193-32.92335,38.51084-32.92335,38.51084,0,0-44.99289,21.68242-70.52913,55.88934C23.29374,218.98539,8.70899,252.47748,0,290.56468v62.79464h144.55516v-94.53497h4.12984l24.78213,94.53497h161.07607l-72.5071-159.24145Z" style="fill:#fcf5f3;"/></g><path d="M615.38918,0h-189.98648l-90.86418,353.35931h148.685l4.59044-42.21956h65.16396l4.58888,42.21956h148.68656L615.38918,0ZM561.10669,138.27405l-14.62209,89.89785c-.2126.75042-.89766,1.26859-1.67765,1.26859h-17.93032c-.84476,0-1.56814-.6057-1.71655-1.43723l-3.03397-37.78104c-.15269-.85468-.91089-1.46699-1.7786-1.43607-1.01009.03598-1.77821.92062-1.67181,1.92584l-3.6422,36.80149c.10893,1.02974-.69849,1.92701-1.73386,1.92701h-19.2526c-.86888,0-1.6051-.63974-1.72647-1.50006l-12.68478-89.89785c-.14841-1.05172.67009-1.99081,1.73231-1.98711h78.06678c1.15422.00389,1.98614,1.10793,1.67181,2.21858Z" style="fill:#fcf5f3;"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.2 KiB |
Reference in New Issue
Block a user