5 Commits

12 changed files with 1519 additions and 1512 deletions

853
package-lock.json generated

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -187,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();

View File

@@ -9,7 +9,7 @@ import type { sign } from 'node:crypto'
<div class="hero-bg" aria-hidden="true"></div> <div class="hero-bg" aria-hidden="true"></div>
<div class="container py-5 py-md-6 position-relative"> <div class="container py-5 py-md-6 position-relative">
<div class="row align-items-center g-5"> <div class="row align-items-center g-5">
<div class="col-xl-6"> <div class="col-12 col-xl-6">
<p class="eyebrow text-purple-light mb-3">Pokémon Card Price Aggregator</p> <p class="eyebrow text-purple-light mb-3">Pokémon Card Price Aggregator</p>
<h1 class="display-4 fw-bold lh-sm mb-4"> <h1 class="display-4 fw-bold lh-sm mb-4">
The home of</br> The home of</br>
@@ -39,13 +39,13 @@ import type { sign } from 'node:crypto'
<div class="col-xl-6 d-none d-xl-block" aria-hidden="true"> <div class="col-xl-6 d-none d-xl-block" aria-hidden="true">
<div class="hero-cards-mockup"> <div class="hero-cards-mockup">
<div class="mockup-card mockup-card--1 shadow-lg rounded-4"> <div class="mockup-card mockup-card--1 shadow-lg rounded-4">
<img class="img-fluid" src="/static/cards/124125.jpg" alt="Imakuni?'s Doduo - XY - Evolutions (EVO)" /> <img class="img-fluid" src="/static/cards/124125.jpg" alt="Sample Pokémon Card" />
</div> </div>
<div class="mockup-card mockup-card--2 shadow-lg rounded-4"> <div class="mockup-card mockup-card--2 shadow-lg rounded-4">
<img class="img-fluid" src="/static/cards/88875.jpg" alt="Sabrina's Gengar - Gym Challenge (G2)" /> <img class="img-fluid" src="/static/cards/88875.jpg" alt="Sample Pokémon Card" />
</div> </div>
<div class="mockup-card mockup-card--3 shadow-lg rounded-4"> <div class="mockup-card mockup-card--3 shadow-lg rounded-4">
<img class="img-fluid" src="/static/cards/567429.jpg" alt="Squirtle - SV07: Stellar Crown (SCR)" /> <img class="img-fluid" src="/static/cards/567429.jpg" alt="Sample Pokémon Card" />
</div> </div>
<div class="price-chip price-chip--nm">NM <strong>$114.99</strong></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 class="price-chip price-chip--lp">LP <strong>$85.66</strong></div>
@@ -53,4 +53,38 @@ import type { sign } from 'node:crypto'
</div> </div>
</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>

View File

@@ -38,12 +38,12 @@ import { Show } from '@clerk/astro/components'
> >
<div class="input-group"> <div class="input-group">
{Astro.url.pathname === '/pokemon' && ( {Astro.url.pathname === '/pokemon' && (
<button class="btn btn-purple" data-bs-toggle="offcanvas" href="#filterBar" type="button" role="button" aria-controls="filterBar" aria-label="filter"> <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"> <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> <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>
<span class="d-none d-md-block fw-medium">Filters</span> <span class="d-none d-md-block fw-medium">Filters</span>
</button> </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="" />

View File

@@ -1,11 +1,21 @@
import { clerkMiddleware, createRouteMatcher, clerkClient } from '@clerk/astro/server'; import { clerkMiddleware, createRouteMatcher, clerkClient } from '@clerk/astro/server';
import type { MiddlewareNext } from 'astro';
import 'dotenv/config';
declare global {
namespace App {
interface Locals {
canAddInventory: boolean;
}
}
}
const isProtectedRoute = createRouteMatcher(['/pokemon']); const isProtectedRoute = createRouteMatcher(['/pokemon']);
const isAdminRoute = createRouteMatcher(['/admin']); const isAdminRoute = createRouteMatcher(['/admin']);
const TARGET_ORG_ID = "org_3Baav9czkRLLlC7g89oJWqRRulK"; const TARGET_ORG_ID = "org_3Baav9czkRLLlC7g89oJWqRRulK";
export const onRequest = clerkMiddleware(async (auth, context) => { export const onRequest = clerkMiddleware(async (auth, context, next) => {
const { isAuthenticated, userId, redirectToSignIn, has } = auth(); const { isAuthenticated, userId, redirectToSignIn, has } = auth();
if (!isAuthenticated && isProtectedRoute(context.request)) { if (!isAuthenticated && isProtectedRoute(context.request)) {
@@ -14,16 +24,18 @@ export const onRequest = clerkMiddleware(async (auth, context) => {
// ── Inventory visibility check ────────────────────────────────────────────── // ── Inventory visibility check ──────────────────────────────────────────────
// Resolves to true if the user belongs to the target org OR has the feature // Resolves to true if the user belongs to the target org OR has the feature
const canAddInventory = const canAddInventory = process.env.INVENTORY_ACCESS === 'true' ||
isAuthenticated &&
userId &&
( (
has({ permission: "org:feature:inventory_add" }) || // Clerk feature flag isAuthenticated &&
(await getUserOrgIds(context, userId)).includes(TARGET_ORG_ID) 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 // Expose the flag to your Astro pages via locals
context.locals.canAddInventory = canAddInventory ?? false; context.locals.canAddInventory = Boolean(canAddInventory);
// ── Admin route guard (unchanged) ─────────────────────────────────────────── // ── Admin route guard (unchanged) ───────────────────────────────────────────
if (isAdminRoute(context.request)) { if (isAdminRoute(context.request)) {
@@ -49,6 +61,8 @@ export const onRequest = clerkMiddleware(async (auth, context) => {
return context.redirect("/"); return context.redirect("/");
} }
} }
return next();
}); });
// ── Helper: fetch all org IDs the current user belongs to ─────────────────── // ── Helper: fetch all org IDs the current user belongs to ───────────────────

View File

@@ -19,7 +19,7 @@ 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">
<div class="container-fluid container-sm mt-4" slot="page"> <div class="container-fluid container-sm mt-5" slot="page">
<div class="row mb-4"> <div class="row mb-4">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<h1 class="mb-4">404 - Page Not Found</h1> <h1 class="mb-4">404 - Page Not Found</h1>
@@ -79,32 +79,6 @@ const pokemonName = pokemon?.Name || "Unknown Pokémon";
<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');

View File

@@ -4,7 +4,8 @@ import Layout from '../layouts/Main.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
--- ---
<Layout title="Contact Us"> <Layout title="Contact Us">
<div class="row mb-4" slot="page"> <div class="container-fluid container-sm my-5" slot="page">
<div class="row mb-4">
<div class="col-12"> <div class="col-12">
<h1>Contact Us</h1> <h1>Contact Us</h1>
</div> </div>
@@ -37,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 -->
@@ -49,6 +50,7 @@ import Footer from '../components/Footer.astro';
</div> </div>
</div> </div>
</div> </div>
</div>
<Footer slot="footer" /> <Footer slot="footer" />
</Layout> </Layout>

View File

@@ -1,6 +1,7 @@
--- ---
import Layout from "../layouts/Main.astro"; import Layout from "../layouts/Main.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 { db } from '../db/index';
import { inventory, skus } from '../db/schema'; import { inventory, skus } from '../db/schema';
@@ -28,13 +29,14 @@ const totalGain = summary.totalGain || 0;
--- ---
<Layout title="Inventory Dashboard"> <Layout title="Inventory Dashboard">
<div class="container-fluid container-sm mt-3" slot="page">
<div class="row g-0" style="min-height: calc(100vh - 120px)" slot="page"> <BackToTop />
<div class="row mb-4">
<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"
@@ -44,7 +46,7 @@ const totalGain = summary.totalGain || 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"
@@ -52,7 +54,7 @@ const totalGain = summary.totalGain || 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) => (
@@ -70,7 +72,7 @@ const totalGain = summary.totalGain || 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>
@@ -78,49 +80,50 @@ const totalGain = summary.totalGain || 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 type="submit"
id="clearSearch" class="btn btn-purple border-start-0"
type="button" aria-label="search"
class="btn btn-sm p-0 position-absolute top-50 end-0 translate-middle-y me-2 text-secondary d-none" onclick="
style="line-height:1" const i = this.closest('form').querySelector('[name=i]').value;
aria-label="Clear search" dataLayer.push({ event: 'view_inventory_results', search_term: i });
>✕</button> 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>
@@ -204,8 +207,8 @@ const totalGain = summary.totalGain || 0;
</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">
@@ -287,288 +290,5 @@ const totalGain = summary.totalGain || 0;
</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>

View File

@@ -19,19 +19,19 @@ import NavItems from '../components/NavItems.astro';
<div class="container"> <div class="container">
<ul class="list-unstyled d-flex flex-wrap justify-content-center justify-content-md-between gap-4 mb-0 text-center"> <ul class="list-unstyled d-flex flex-wrap justify-content-center justify-content-md-between gap-4 mb-0 text-center">
<li> <li>
<strong class="d-block fs-4 fw-bold text-aqua">Pokémon TCG</strong> <strong class="d-block fs-4 fw-bold text-orchid">Pokémon TCG</strong>
<span class="text-body-secondary small">EN · JP Languages</span> <span class="text-body-secondary small">All EN and JP Sets</span>
</li> </li>
<li> <li>
<strong class="d-block fs-4 fw-bold text-aqua">All Conditions</strong> <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> <span class="text-body-secondary small">NM · LP · MP · HP · DMG</span>
</li> </li>
<li> <li>
<strong class="d-block fs-4 fw-bold text-aqua">Real-Time</strong> <strong class="d-block fs-4 fw-bold text-orchid">Real-Time</strong>
<span class="text-body-secondary small">Accurate Market Prices</span> <span class="text-body-secondary small">Accurate Market Prices</span>
</li> </li>
<li> <li>
<strong class="d-block fs-4 fw-bold text-aqua">100% Free</strong> <strong class="d-block fs-4 fw-bold text-orchid">100% Free</strong>
<span class="text-body-secondary small">Pricing Features Always Free</span> <span class="text-body-secondary small">Pricing Features Always Free</span>
</li> </li>
</ul> </ul>
@@ -52,7 +52,7 @@ import NavItems from '../components/NavItems.astro';
<article class="col-md-5 offset-md-1"> <article class="col-md-5 offset-md-1">
<div class="feature-card h-100 p-4 rounded-3"> <div class="feature-card h-100 p-4 rounded-3">
<div class="feature-icon mb-3" aria-hidden="true"> <div class="feature-icon mb-3" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16"><path d="M1 2.828c.885-.37 2.154-.769 3.388-.893 1.33-.134 2.458.063 3.112.752v9.746c-.935-.53-2.12-.603-3.213-.493-1.18.12-2.37.461-3.287.811zm7.5-.141c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783"/></svg> <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>
<h3 class="h5 fw-semibold mb-2">Complete Card Database</h3> <h3 class="h5 fw-semibold mb-2">Complete Card Database</h3>
<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> <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>
@@ -62,7 +62,7 @@ import NavItems from '../components/NavItems.astro';
<article class="col-md-5"> <article class="col-md-5">
<div class="feature-card h-100 p-4 rounded-3"> <div class="feature-card h-100 p-4 rounded-3">
<div class="feature-icon mb-3" aria-hidden="true"> <div class="feature-icon mb-3" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16"><path d="M0 0h1v15h15v1H0zm14.817 3.113a.5.5 0 0 1 .07.704l-4.5 5.5a.5.5 0 0 1-.74.037L7.06 6.767l-3.656 5.027a.5.5 0 0 1-.808-.588l4-5.5a.5.5 0 0 1 .758-.06l2.609 2.61 4.15-5.073a.5.5 0 0 1 .704-.07"/></svg> <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> </div>
<h3 class="h5 fw-semibold mb-2">Condition-Graded Pricing</h3> <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> <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>
@@ -72,7 +72,7 @@ import NavItems from '../components/NavItems.astro';
<article class="col-md-5 offset-md-1"> <article class="col-md-5 offset-md-1">
<div class="feature-card h-100 p-4 rounded-3"> <div class="feature-card h-100 p-4 rounded-3">
<div class="feature-icon mb-3" aria-hidden="true"> <div class="feature-icon mb-3" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16"><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.93-9.412-1 4.705c-.07.34.029.533.304.533.194 0 .487-.07.686-.246l-.088.416c-.287.346-.92.598-1.465.598-.703 0-1.002-.422-.808-1.319l.738-3.468c.064-.293.006-.399-.287-.47l-.451-.081.082-.381 2.29-.287zM8 5.5a1 1 0 1 1 0-2 1 1 0 0 1 0 2"/></svg> <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> </div>
<h3 class="h5 fw-semibold mb-2">All Variants</h3> <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> <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>
@@ -82,7 +82,7 @@ import NavItems from '../components/NavItems.astro';
<article class="col-md-5"> <article class="col-md-5">
<div class="feature-card h-100 p-4 rounded-3"> <div class="feature-card h-100 p-4 rounded-3">
<div class="feature-icon mb-3" aria-hidden="true"> <div class="feature-icon mb-3" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" fill="currentColor" viewBox="0 0 16 16"><path d="M8 3.5a.5.5 0 0 0-1 0V9a.5.5 0 0 0 .252.434l3.5 2a.5.5 0 0 0 .496-.868L8 8.71z"/><path d="M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m7-8A7 7 0 1 1 1 8a7 7 0 0 1 14 0"/></svg> <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> </div>
<h3 class="h5 fw-semibold mb-2">Fast Search, Instant Results</h3> <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> <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>
@@ -96,10 +96,10 @@ import NavItems from '../components/NavItems.astro';
═══════════════════════════════════════════ --> ═══════════════════════════════════════════ -->
<section class="premium-section py-6" aria-labelledby="premium-heading"> <section class="premium-section py-6" aria-labelledby="premium-heading">
<div class="container"> <div class="container">
<div class="row align-items-center g-5"> <div class="row align-items-center g-4">
<div class="col-lg-5"> <div class="col-lg-5">
<p class="eyebrow text-purple-light mb-3">Coming Soon · Premium</p> <p class="eyebrow text-lilac mb-3">Coming Soon · Premium</p>
<h2 id="premium-heading" class="h1 fw-bold mb-3"> <h2 id="premium-heading" class="h1 fw-bold mb-3">
<span class="text-gradient">Your collection,<br/>fully managed.</span> <span class="text-gradient">Your collection,<br/>fully managed.</span>
</h2> </h2>
@@ -161,7 +161,6 @@ import NavItems from '../components/NavItems.astro';
FINAL CTA FINAL CTA
═══════════════════════════════════════════ --> ═══════════════════════════════════════════ -->
<section class="cta-section py-6" aria-labelledby="cta-heading"> <section class="cta-section py-6" aria-labelledby="cta-heading">
<div class="hero-bg" aria-hidden="true"></div>
<div class="container text-center"> <div class="container text-center">
<h2 id="cta-heading" class="display-5 fw-bold mb-3">Ready to join the RAT Pack?</h2> <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;"> <p class="lead text-body-secondary mb-4 mx-auto" style="max-width: 520px;">

View File

@@ -182,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) &&
@@ -203,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>

View File

@@ -5,6 +5,7 @@ import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
export const prerender = false; export const prerender = false;
import * as util from 'util'; import * as util from 'util';
import { db } from '../../db';
// get the query from post request using form data // get the query from post request using form data
const formData = await Astro.request.formData(); const formData = await Astro.request.formData();
@@ -13,16 +14,25 @@ const start = Number(formData.get('start')?.toString() || '0');
const { userId } = Astro.locals.auth(); 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) // primary search values (for cards)
let searchArray = [{ let searchArray = [{
collection: 'inventories', collection: 'inventories',
filter_by: `userId:=${userId} && $skus($cards(id:*))`, filter_by: `userId:=${userId}`,
per_page: 20, per_page: 20,
facet_by: '', facet_by: '',
max_facet_values: 0, max_facet_values: 0,
page: Math.floor(start / 20) + 1, page: Math.floor(start / 20) + 1,
include_fields: '$skus(*),$cards(*)', include_fields: 'id',
}]; }];
// on first load (start === 0) we want to get the facets for the filters // on first load (start === 0) we want to get the facets for the filters
@@ -54,58 +64,81 @@ const commonSearchParams = {
// use typesense to search for cards matching the query and return the productIds of the results // use typesense to search for cards matching the query and return the productIds of the results
const searchResults = await client.multiSearch.perform(searchRequests, commonSearchParams); const searchResults = await client.multiSearch.perform(searchRequests, commonSearchParams);
const inventoryResults = searchResults.results[0] as any; const inventoryResults = searchResults.results[0] as any;
console.log('inventoryResults', util.inspect(inventoryResults, { depth: null })); // console.log('inventoryResults', util.inspect(inventoryResults, { depth: null }));
const pokemon = inventoryResults.hits?.map((hit: any) => hit.document) ?? []; const pokemon = inventoryResults.hits ?
await Promise.all(
inventoryResults.hits.map(
async (hit: any) => { return (await InventoryDetails(hit.document.id)); }
)
) : [];
const totalHits = inventoryResults?.found; const totalHits = inventoryResults?.found;
// console.log('pokemon', util.inspect(pokemon, { depth: null }));
console.log(`totalHits: ${totalHits}`); console.log(`totalHits: ${totalHits}`);
--- ---
{pokemon.map((inventory:any) => { {pokemon.map((inventory:any) => {
const sku = inventory.skus; const sku = inventory.sku;
const card = inventory.cards; const card = sku.card;
const market = sku.marketPrice/100 || 0; const market = Number(sku.marketPrice) || 0;
const purchase = inventory.purchasePrice/100 || 0; const purchase = Number(inventory.purchasePrice) || 0;
const diff = market - purchase; const diff = market - purchase;
const pct = purchase > 0 ? (diff / purchase) * 100 : 0; const pct = purchase > 0 ? (diff / purchase) * 100 : 0;
const isGain = diff >= 0; const isGain = diff >= 0;
return ( return (
<div class="col"> <div class="col equal-height-col">
<article class="inv-grid-card"> <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="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 h-100">
<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')" /> <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"> <span class="position-absolute top-50 start-0 d-inline medium-icon" style="z-index:4">
<FirstEditionIcon edition={card.variant} /> <FirstEditionIcon edition={card.variant} />
</span> </span>
</div> </div>
</div> </div>
<div class="d-flex flex-row justify-content-between my-1 align-items-center"> <div class="d-flex flex-row justify-content-between my-1 align-items-center edit-bar">
<input type="number" class="form-control text-center" style="max-width: 33%;" value="1" min="1" max="999" aria-label="Quantity input" aria-describedby="button-minus button-plus"> <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"> <div class="" aria-label="Edit controls">
<button type="button" class="btn btn-outline-warning me-2"><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-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-outline-danger"><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>
<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> </div>
<div class="d-flex flex-row mt-1"> <div class="d-flex flex-row mt-1">
<div class="p fs-7 text-body-tertiary">{card.setName}</div> <div class="text-body-tertiary flex-grow-1 small">{card.setName}</div>
</div> </div>
<div class="d-flex flex-row mt-1"> <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="fs-6 fw-semibold my-0">{card.productName}</div>
<div class="small my-0">{card.number}</div>
</div> </div>
<div class="d-flex flex-row mt-1 justify-content-between align-items-baseline"> <div class="d-flex flex-row justify-content-between align-items-baseline">
<div class={`inv-grid-trend small ${isGain ? "up" : "down"}`}> <div class="text-body-secondary">
<span class="inv-grid-arrow">{isGain ? "▲" : "▼"}</span> <span class="small">Purchase</span>
<span class="h6 my-0">${market.toFixed(2)}</span>
</div> </div>
<div class={`inv-grid-delta small ${isGain ? "up" : "down"}`}> <div class="text-body-secondary">
{isGain ? "+" : "-"}${Math.abs(diff).toFixed(2)}</br>{isGain ? "+" : "-"}{Math.abs(pct).toFixed(2)}% <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> </div>
</article>
</div> </div>
); );