diff --git a/src/assets/css/main.scss b/src/assets/css/main.scss
index 32dbfa1..01177d1 100644
--- a/src/assets/css/main.scss
+++ b/src/assets/css/main.scss
@@ -278,6 +278,40 @@ $tiers: (
}
}
+// ── Inventory form condition buttons ──────────────────────────────────────
+// Reuses $tiers map so colors stay in sync with nav tabs and price-row
+
+$cond-text: (
+ nm: rgba(156, 204, 102, 1),
+ lp: rgba(211, 225, 86, 1),
+ mp: rgba(255, 238, 87, 1),
+ hp: rgba(255, 201, 41, 1),
+ dmg: rgba(255, 167, 36, 1),
+);
+
+@each $name, $color in $tiers {
+ @if map-has-key($cond-text, $name) {
+ .btn-check:checked + .btn-cond-#{$name} {
+ background-color: $color;
+ border-color: $color;
+ color: rgba(0, 0, 0, 0.94);
+ }
+
+ .btn-cond-#{$name} {
+ border-color: rgba($color, 0.4);
+ color: var(--bs-body-color);
+ background: transparent;
+ font-weight: 600;
+ transition: background-color 0.1s, border-color 0.1s;
+ }
+
+ .btn-check:not(:checked) + .btn-cond-#{$name}:hover {
+ background-color: rgba($color, 0.67);
+ border-color: transparent;
+ }
+ }
+ }
+
/* --------------------------------------------------
Misc UI
-------------------------------------------------- */
@@ -430,12 +464,6 @@ $tiers: (
color: #fff;
}
-#inventoryForm .btn-check:checked + .nav-link {
- outline: 2px solid rgba(0, 0, 0, 0.4);
- outline-offset: -2px;
-}
-#inventoryForm .nav-link { cursor: pointer; }
-
.fs-7 {
font-size: 0.9rem !important;
}
diff --git a/src/assets/js/main.js b/src/assets/js/main.js
index abc4f3a..316f997 100644
--- a/src/assets/js/main.js
+++ b/src/assets/js/main.js
@@ -2,51 +2,97 @@ import * as bootstrap from 'bootstrap';
window.bootstrap = bootstrap;
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
-// trap browser back and close the modal if open
-const cardModal = document.getElementById('cardModal');
-const loadingMsg = cardModal.innerHTML;
-// Push a new history state when the modal is shown
-cardModal.addEventListener('shown.bs.modal', () => {
- history.pushState({ modalOpen: true }, null, '#cardModal');
-});
-// Listen for the browser's back button (popstate event)
-window.addEventListener('popstate', (e) => {
- if (cardModal.classList.contains('show')) {
- const modalInstance = bootstrap.Modal.getInstance(cardModal);
- if (modalInstance) {
- modalInstance.hide();
+document.addEventListener('DOMContentLoaded', () => {
+
+ // Initialize all Bootstrap modals
+ document.querySelectorAll('.modal').forEach(modalEl => {
+ bootstrap.Modal.getOrCreateInstance(modalEl);
+ });
+
+ // Initialize tooltips
+ document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
+ if (!el._tooltipInstance) {
+ el._tooltipInstance = new bootstrap.Tooltip(el, { container: 'body' });
}
- }
-});
-// Trigger a back navigation when the modal is closed via its native controls (X, backdrop click)
-cardModal.addEventListener('hide.bs.modal', () => {
- cardModal.innerHTML = loadingMsg;
- if (history.state && history.state.modalOpen) {
- history.back();
- }
-});
+ });
+ // ---------------- DASHBOARD LOGIC ----------------
+ const toggleBtn = document.getElementById("toggleViewBtn");
+ const gridView = document.getElementById("gridView");
+ const tableView = document.getElementById("tableView");
+ const searchInput = document.getElementById("inventorySearch");
+ const tbody = document.getElementById("inventoryRows");
- import { Tooltip } from "bootstrap";
-
- // Initialize all tooltips globally
- const initTooltips = () => {
- document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
- if (!el._tooltipInstance) {
- el._tooltipInstance = new Tooltip(el, {
- container: 'body', // ensures tooltip is appended to body, important for modals
- });
+ if(toggleBtn && gridView && tableView && tbody) {
+ // TOGGLE GRID/TABLE
+ toggleBtn.addEventListener("click", () => {
+ if(gridView.style.display !== "none") {
+ gridView.style.display = "none";
+ tableView.style.display = "block";
+ toggleBtn.textContent = "Switch to Grid View";
+ } else {
+ gridView.style.display = "block";
+ tableView.style.display = "none";
+ toggleBtn.textContent = "Switch to Table View";
}
});
- };
- // Run on page load
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', initTooltips);
- } else {
- initTooltips();
+ // SEARCH FILTER
+ if(searchInput) {
+ searchInput.addEventListener("input", e => {
+ const term = e.target.value.toLowerCase();
+ [...tbody.querySelectorAll("tr")].forEach(row => {
+ row.style.display = row.textContent.toLowerCase().includes(term) ? "" : "none";
+ });
+ });
+ }
+
+ // SORTING
+ document.querySelectorAll("th[data-key]").forEach(th => {
+ let sortAsc = true;
+ th.addEventListener("click", () => {
+ const key = th.dataset.key;
+ const indexMap = {name:0,set:1,condition:2,qty:3,price:4,market:5,gain:6};
+ const idx = indexMap[key];
+ const rows = [...tbody.querySelectorAll("tr")];
+
+ rows.sort((a,b) => {
+ let aText = a.children[idx].textContent.replace(/\$|,/g,'').toLowerCase();
+ let bText = b.children[idx].textContent.replace(/\$|,/g,'').toLowerCase();
+ if(!isNaN(aText) && !isNaN(bText)) return sortAsc ? aText-bText : bText-aText;
+ return sortAsc ? aText.localeCompare(bText) : bText.localeCompare(aText);
+ });
+
+ sortAsc = !sortAsc;
+ tbody.innerHTML="";
+ rows.forEach(r => tbody.appendChild(r));
+ });
+ });
+
+ // INLINE EDITING + GAIN/LOSS UPDATE
+ tbody.addEventListener("input", e => {
+ const row = e.target.closest("tr");
+ if(!row) return;
+
+ const priceCell = row.querySelector(".editable-price");
+ const qtyCell = row.querySelector(".editable-qty");
+ const marketCell = row.children[5];
+ const gainCell = row.querySelector(".gain");
+
+ if(e.target.classList.contains("editable-price")) {
+ e.target.textContent = e.target.textContent.replace(/[^\d.]/g,"");
+ }
+ if(e.target.classList.contains("editable-qty")) {
+ e.target.textContent = e.target.textContent.replace(/\D/g,"");
+ }
+
+ const price = parseFloat(priceCell.textContent) || 0;
+ const qty = parseInt(qtyCell.textContent) || 0;
+ const market = parseFloat(marketCell.textContent) || 0;
+ const gain = market - price;
+
+ gainCell.textContent = (gain>=0 ? "+" : "-") + Math.abs(gain);
+ gainCell.className = gain>=0 ? "gain text-success" : "gain text-danger";
+ });
}
-
- // Optional: observe DOM changes for dynamically added tooltips (e.g., modals loaded later)
- const observer = new MutationObserver(() => initTooltips());
- observer.observe(document.body, { childList: true, subtree: true });
\ No newline at end of file
+});
\ No newline at end of file
diff --git a/src/components/CardGrid.astro b/src/components/CardGrid.astro
index 209439c..05d33f8 100644
--- a/src/components/CardGrid.astro
+++ b/src/components/CardGrid.astro
@@ -44,15 +44,54 @@ import BackToTop from "./BackToTop.astro"
-
-
\ No newline at end of file
diff --git a/src/components/InventoryTable.astro b/src/components/InventoryTable.astro
new file mode 100644
index 0000000..c8b9a1e
--- /dev/null
+++ b/src/components/InventoryTable.astro
@@ -0,0 +1,37 @@
+---
+const mockInventory = [
+ { name: "Charizard", set: "Base Set", condition: "NM", qty: 2, price: 350, market: 400, gain: 50 },
+ { name: "Pikachu", set: "Shining Legends", condition: "LP", qty: 5, price: 15, market: 20, gain: 5 },
+];
+---
+
+
+
+
+
+ | Card |
+ Set |
+ Condition |
+ Qty |
+ Price |
+ Market |
+ Gain/Loss |
+
+
+
+ {mockInventory.map(card => (
+
+ | {card.name} |
+ {card.set} |
+ {card.condition} |
+ {card.qty} |
+ ${card.price} |
+ ${card.market} |
+ = 0 ? "text-success" : "text-danger"}>
+ {card.gain >= 0 ? "+" : "-"}${Math.abs(card.gain)}
+ |
+
+ ))}
+
+
+
\ No newline at end of file
diff --git a/src/pages/dashboard.astro b/src/pages/dashboard.astro
new file mode 100644
index 0000000..ff26a86
--- /dev/null
+++ b/src/pages/dashboard.astro
@@ -0,0 +1,1189 @@
+---
+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 FirstEditionIcon from "../components/FirstEditionIcon.astro";
+
+// 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: "3830",
+ 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: "3959",
+ 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: "3729",
+ productName: "Mewtwo",
+ setName: "Fossil",
+ setCode: "FO",
+ number: "10/62",
+ rarityName: "Rare Holo",
+ energyType: "Psychic",
+ variant: "Normal",
+ qty: 1,
+ purchasePrice: 12000,
+ skus: [
+ { condition: "Near Mint", marketPrice: 13000 },
+ { condition: "Lightly Played", marketPrice: 10000 },
+ { condition: "Moderately Played", marketPrice: 7200 },
+ { condition: "Heavily Played", marketPrice: 4500 },
+ { condition: "Damaged", marketPrice: 2500 },
+ ],
+ },
+ {
+ productId: "4114",
+ 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: "3887",
+ 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: "4201",
+ 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: "4055",
+ 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: "3991",
+ 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: "4301",
+ 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: "4302",
+ productName: "Venusaur",
+ setName: "Base Set",
+ setCode: "BS",
+ number: "15/102",
+ rarityName: "Rare Holo",
+ energyType: "Grass",
+ variant: "1st Edition",
+ qty: 1,
+ purchasePrice: 22000,
+ skus: [
+ { condition: "Near Mint", marketPrice: 19500 },
+ { condition: "Lightly Played", marketPrice: 15000 },
+ { condition: "Moderately Played", marketPrice: 10500 },
+ { condition: "Heavily Played", marketPrice: 6500 },
+ { condition: "Damaged", marketPrice: 3500 },
+ ],
+ },
+ {
+ productId: "4303",
+ 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: "4304",
+ 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: "4305",
+ 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: "4306",
+ productName: "Alakazam",
+ setName: "Base Set",
+ setCode: "BS",
+ number: "1/102",
+ rarityName: "Rare Holo",
+ energyType: "Psychic",
+ variant: "Shadowless",
+ qty: 2,
+ purchasePrice: 9500,
+ skus: [
+ { condition: "Near Mint", marketPrice: 12000 },
+ { condition: "Lightly Played", marketPrice: 9300 },
+ { condition: "Moderately Played", marketPrice: 6600 },
+ { condition: "Heavily Played", marketPrice: 4100 },
+ { condition: "Damaged", marketPrice: 2200 },
+ ],
+ },
+ {
+ productId: "4307",
+ productName: "Sylveon VMAX",
+ setName: "Evolving Skies",
+ setCode: "EVS",
+ number: "212/203",
+ rarityName: "Secret Rare",
+ energyType: "Psychic",
+ variant: "Alternate Art",
+ qty: 1,
+ purchasePrice: 6800,
+ skus: [
+ { condition: "Near Mint", marketPrice: 8500 },
+ { condition: "Lightly Played", marketPrice: 6700 },
+ { condition: "Moderately Played", marketPrice: 4800 },
+ { condition: "Heavily Played", marketPrice: 3000 },
+ { condition: "Damaged", marketPrice: 1500 },
+ ],
+ },
+ {
+ productId: "4308",
+ 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: "4309",
+ 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: "4310",
+ 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: "4311",
+ productName: "Snorlax",
+ setName: "Celebrations",
+ setCode: "CEL",
+ number: "005/025",
+ rarityName: "Rare Holo",
+ energyType: "Colorless",
+ variant: "Classic Collection",
+ qty: 6,
+ purchasePrice: 600,
+ skus: [
+ { condition: "Near Mint", marketPrice: 750 },
+ { condition: "Lightly Played", marketPrice: 580 },
+ { condition: "Moderately Played", marketPrice: 420 },
+ { condition: "Heavily Played", marketPrice: 260 },
+ { condition: "Damaged", marketPrice: 130 },
+ ],
+ },
+ {
+ productId: "4312",
+ productName: "Articuno",
+ setName: "Fossil",
+ setCode: "FO",
+ number: "2/62",
+ rarityName: "Rare Holo",
+ energyType: "Water",
+ variant: "1st Edition",
+ qty: 1,
+ purchasePrice: 14000,
+ skus: [
+ { condition: "Near Mint", marketPrice: 11500 },
+ { condition: "Lightly Played", marketPrice: 9000 },
+ { condition: "Moderately Played", marketPrice: 6400 },
+ { condition: "Heavily Played", marketPrice: 4000 },
+ { condition: "Damaged", marketPrice: 2200 },
+ ],
+ },
+ {
+ productId: "4313",
+ productName: "Radiant Charizard",
+ setName: "Pokemon GO",
+ setCode: "PGO",
+ number: "011/078",
+ rarityName: "Radiant Rare",
+ energyType: "Fire",
+ variant: "Normal",
+ qty: 2,
+ purchasePrice: 2800,
+ skus: [
+ { condition: "Near Mint", marketPrice: 4200 },
+ { condition: "Lightly Played", marketPrice: 3300 },
+ { condition: "Moderately Played", marketPrice: 2400 },
+ { condition: "Heavily Played", marketPrice: 1500 },
+ { condition: "Damaged", marketPrice: 800 },
+ ],
+ },
+ {
+ productId: "4314",
+ 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 totalValue = inventory.reduce((s, c) => s + nmPrice(c) * c.qty, 0);
+const totalGain = inventory.reduce((s, c) => s + gain(c) * c.qty, 0);
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {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 (
+
+
+
+
+
+
+
+
+
+ {isGain ? "▲" : "▼"}
+ ${market.toFixed(2)}
+
+
+ {isGain ? "+" : "-"}${Math.abs(diff).toFixed(2)} ({isGain ? "+" : "-"}{Math.abs(pct).toFixed(2)}%)
+
+
+
+
+
+
+ );
+ })}
+
+
+
+
+
+
+ {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 (
+
+
+
+
+ 
+
+
+
+
+ {card.productName}
+
+
+
+
+
+ Near Mint
+ •
+ {card.variant !== "Normal" ? card.variant : "Holofoil"}
+
+
+
+
+
+ {isGain ? "▲" : "▼"}
+ ${market.toFixed(2)}
+
+
+ {isGain ? "+" : "-"}${Math.abs(diff).toFixed(2)} ({isGain ? "+" : "-"}{Math.abs(pct).toFixed(2)}%)
+
+ Qty: {card.qty}
+
+
+ |
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Upload a CSV exported from Collectr, TCGPlayer, or any marketplace. Columns: name, set, condition, qty, price, market.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Select a card to edit its quantity and purchase price.
+
+
+
+
+
+
+
+
+
+
+
+
+
Search results will appear here. Connect to your card database API to enable live search.
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/pages/partials/card-modal.astro b/src/pages/partials/card-modal.astro
index cd716d1..8ee705e 100644
--- a/src/pages/partials/card-modal.astro
+++ b/src/pages/partials/card-modal.astro
@@ -338,157 +338,265 @@ const altSearchUrl = (card: any) => {
})}
-
-
-
-
-
Required.
-
+
+ $
+
+ %
+
-
-
-
-
-
+
Enter the purchase price.
+
Enter a purchase price.
+
-
-
+
+
+
+
+
+
0 / 255
+
+
+
+
+
+
+
+
+
+
+
+
Inventory entries for {card?.productName}
+
+
+
+
No inventory entries yet
+
+ Once you add copies of this card, they’ll show up here.
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Purchase price
+
$8.50
+
+
+
Market price
+
$10.25
+
+
+
+
+
+
+
+
Qty
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Purchase price
+
$6.00
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
- $
-
- %
-
-
Enter the amount you paid.
-
Enter a purchase price.
-
-
-
-
-
0 / 255
-
-
-
-
-
-
-
-
-
-
+