style tweaks to both form and existing inventory, added createdAt and modified purchasePrice (for % of market)
This commit is contained in:
132
drizzle/20260405194139_orange_jack_murdock/migration.sql
Normal file
132
drizzle/20260405194139_orange_jack_murdock/migration.sql
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
CREATE SCHEMA "pokemon";
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "pokemon"."cards" (
|
||||||
|
"card_id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name "pokemon"."cards_card_id_seq" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),
|
||||||
|
"product_id" integer NOT NULL,
|
||||||
|
"variant" varchar(100) NOT NULL,
|
||||||
|
"product_name" varchar(255),
|
||||||
|
"product_line_name" varchar(255),
|
||||||
|
"product_url_name" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"rarity_name" varchar(100),
|
||||||
|
"sealed" boolean DEFAULT false NOT NULL,
|
||||||
|
"set_id" integer,
|
||||||
|
"card_type" varchar(100),
|
||||||
|
"energy_type" varchar(100),
|
||||||
|
"number" varchar(50),
|
||||||
|
"artist" varchar(255)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "pokemon"."inventory" (
|
||||||
|
"inventory_id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
"user_id" varchar(100) NOT NULL,
|
||||||
|
"catalog_name" varchar(100),
|
||||||
|
"card_id" integer NOT NULL,
|
||||||
|
"condition" varchar(255) NOT NULL,
|
||||||
|
"quantity" integer,
|
||||||
|
"purchase_price" numeric(10,2),
|
||||||
|
"note" varchar(255)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "pokemon"."price_history" (
|
||||||
|
"sku_id" integer,
|
||||||
|
"calculated_at" timestamp,
|
||||||
|
"market_price" numeric(10,2),
|
||||||
|
CONSTRAINT "pk_price_history" PRIMARY KEY("sku_id","calculated_at")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "pokemon"."processing_skus" (
|
||||||
|
"sku_id" integer PRIMARY KEY
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "pokemon"."sales_history" (
|
||||||
|
"sku_id" integer,
|
||||||
|
"order_date" timestamp,
|
||||||
|
"title" varchar(255),
|
||||||
|
"custom_listing_id" varchar(255),
|
||||||
|
"language" varchar(100),
|
||||||
|
"listing_type" varchar(100),
|
||||||
|
"purchase_price" numeric(10,2),
|
||||||
|
"quantity" integer,
|
||||||
|
"shipping_price" numeric(10,2),
|
||||||
|
CONSTRAINT "pk_sales_history" PRIMARY KEY("sku_id","order_date")
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "pokemon"."sets" (
|
||||||
|
"set_id" integer PRIMARY KEY,
|
||||||
|
"set_name" varchar(255) NOT NULL,
|
||||||
|
"set_url_name" varchar(255) NOT NULL,
|
||||||
|
"set_code" varchar(100) NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "pokemon"."skus" (
|
||||||
|
"sku_id" integer PRIMARY KEY,
|
||||||
|
"card_id" integer DEFAULT 0 NOT NULL,
|
||||||
|
"product_id" integer NOT NULL,
|
||||||
|
"condition" varchar(255) NOT NULL,
|
||||||
|
"language" varchar(100) NOT NULL,
|
||||||
|
"variant" varchar(100) NOT NULL,
|
||||||
|
"calculated_at" timestamp,
|
||||||
|
"highest_price" numeric(10,2),
|
||||||
|
"lowest_price" numeric(10,2),
|
||||||
|
"market_price" numeric(10,2),
|
||||||
|
"price_count" integer
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "pokemon"."tcg_overrides" (
|
||||||
|
"product_id" integer PRIMARY KEY,
|
||||||
|
"product_name" varchar(255),
|
||||||
|
"product_line_name" varchar(255),
|
||||||
|
"product_url_name" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"rarity_name" varchar(100),
|
||||||
|
"sealed" boolean DEFAULT false NOT NULL,
|
||||||
|
"set_id" integer,
|
||||||
|
"card_type" varchar(100),
|
||||||
|
"energy_type" varchar(100),
|
||||||
|
"number" varchar(50),
|
||||||
|
"artist" varchar(255)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE TABLE "pokemon"."tcg_cards" (
|
||||||
|
"product_id" integer PRIMARY KEY,
|
||||||
|
"product_name" varchar(255) NOT NULL,
|
||||||
|
"product_line_name" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"product_line_url_name" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"product_status_id" integer DEFAULT 0 NOT NULL,
|
||||||
|
"product_type_id" integer DEFAULT 0 NOT NULL,
|
||||||
|
"product_url_name" varchar(255) DEFAULT '' NOT NULL,
|
||||||
|
"rarity_name" varchar(100) DEFAULT '' NOT NULL,
|
||||||
|
"sealed" boolean DEFAULT false NOT NULL,
|
||||||
|
"seller_listable" boolean DEFAULT false NOT NULL,
|
||||||
|
"set_id" integer,
|
||||||
|
"shipping_category_id" integer,
|
||||||
|
"duplicate" boolean DEFAULT false NOT NULL,
|
||||||
|
"foil_only" boolean DEFAULT false NOT NULL,
|
||||||
|
"max_fulfillable_quantity" integer,
|
||||||
|
"total_listings" integer,
|
||||||
|
"score" numeric(10,2),
|
||||||
|
"lowest_price" numeric(10,2),
|
||||||
|
"lowest_price_with_shipping" numeric(10,2),
|
||||||
|
"market_price" numeric(10,2),
|
||||||
|
"median_price" numeric(10,2),
|
||||||
|
"attack1" varchar(1024),
|
||||||
|
"attack2" varchar(1024),
|
||||||
|
"attack3" varchar(1024),
|
||||||
|
"attack4" varchar(1024),
|
||||||
|
"card_type" varchar(100),
|
||||||
|
"card_type_b" varchar(100),
|
||||||
|
"energy_type" varchar(100),
|
||||||
|
"flavor_text" varchar(1000),
|
||||||
|
"hp" integer,
|
||||||
|
"number" varchar(50) DEFAULT '' NOT NULL,
|
||||||
|
"release_date" timestamp,
|
||||||
|
"resistance" varchar(100),
|
||||||
|
"retreat_cost" varchar(100),
|
||||||
|
"stage" varchar(100),
|
||||||
|
"weakness" varchar(100),
|
||||||
|
"artist" varchar(255)
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "idx_card_product_id" ON "pokemon"."cards" ("product_id","variant");--> statement-breakpoint
|
||||||
|
CREATE INDEX "idx_userid_cardid" ON "pokemon"."inventory" ("user_id","card_id");--> statement-breakpoint
|
||||||
|
CREATE INDEX "idx_product_id_condition" ON "pokemon"."skus" ("product_id","variant","condition");--> statement-breakpoint
|
||||||
|
CREATE INDEX "idx_card_id_condition" ON "pokemon"."skus" ("card_id","condition");
|
||||||
1550
drizzle/20260405194139_orange_jack_murdock/snapshot.json
Normal file
1550
drizzle/20260405194139_orange_jack_murdock/snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -172,6 +172,16 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col:has(.image-grow:hover) .inventory-button {
|
||||||
|
opacity: 0.20;
|
||||||
|
transition: opacity 350ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-button {
|
||||||
|
// add transition to existing rule
|
||||||
|
transition: opacity 350ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
.card-modal {
|
.card-modal {
|
||||||
background-color: rgba(1, 11, 18, 0.8);
|
background-color: rgba(1, 11, 18, 0.8);
|
||||||
cursor: default;
|
cursor: default;
|
||||||
@@ -482,7 +492,7 @@ $cond-text: (
|
|||||||
|
|
||||||
.inventory-button {
|
.inventory-button {
|
||||||
margin-bottom: -2.25rem;
|
margin-bottom: -2.25rem;
|
||||||
margin-right: -0.5rem;
|
margin-right: -0.25rem;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,82 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
<script is:inline>
|
<script is:inline>
|
||||||
(function () {
|
(function () {
|
||||||
|
|
||||||
|
// ── Price mode helpers ────────────────────────────────────────────────────
|
||||||
|
// marketPriceByCondition is injected into the modal HTML via a data attribute
|
||||||
|
// on #inventoryEntryList: data-market-prices='{"Near Mint":6.00,...}'
|
||||||
|
// See card-modal.astro for where this is set.
|
||||||
|
|
||||||
|
function getMarketPrices(form) {
|
||||||
|
const listEl = form.closest('.tab-pane')?.querySelector('#inventoryEntryList')
|
||||||
|
?? document.getElementById('inventoryEntryList');
|
||||||
|
try {
|
||||||
|
return JSON.parse(listEl?.dataset.marketPrices || '{}');
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyPriceModeUI(form, mode) {
|
||||||
|
const priceInput = form.querySelector('#purchasePrice');
|
||||||
|
const pricePrefix = form.querySelector('#pricePrefix');
|
||||||
|
const priceSuffix = form.querySelector('#priceSuffix');
|
||||||
|
const priceHint = form.querySelector('#priceHint');
|
||||||
|
if (!priceInput) return;
|
||||||
|
|
||||||
|
const isPct = mode === 'percent';
|
||||||
|
pricePrefix?.classList.toggle('d-none', isPct);
|
||||||
|
priceSuffix?.classList.toggle('d-none', !isPct);
|
||||||
|
priceInput.step = isPct ? '1' : '0.01';
|
||||||
|
priceInput.max = isPct ? '100' : '';
|
||||||
|
priceInput.placeholder = isPct ? '0' : '0.00';
|
||||||
|
priceInput.classList.toggle('rounded-end', !isPct);
|
||||||
|
priceInput.classList.toggle('rounded-start', isPct);
|
||||||
|
|
||||||
|
if (priceHint && !isPct) priceHint.textContent = 'Enter the purchase price.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePriceHint(form) {
|
||||||
|
const priceInput = form.querySelector('#purchasePrice');
|
||||||
|
const priceHint = form.querySelector('#priceHint');
|
||||||
|
if (!priceInput || !priceHint) return;
|
||||||
|
|
||||||
|
const mode = form.querySelector('input[name="priceMode"]:checked')?.value ?? 'dollar';
|
||||||
|
if (mode !== 'percent') { priceHint.textContent = 'Enter the purchase price.'; return; }
|
||||||
|
|
||||||
|
const condition = form.querySelector('input[name="condition"]:checked')?.value ?? 'Near Mint';
|
||||||
|
const prices = getMarketPrices(form);
|
||||||
|
const marketPrice = prices[condition] ?? 0;
|
||||||
|
const pct = parseFloat(priceInput.value) || 0;
|
||||||
|
const resolved = ((pct / 100) * marketPrice).toFixed(2);
|
||||||
|
priceHint.textContent = marketPrice
|
||||||
|
? `= $${resolved} (${pct}% of $${marketPrice.toFixed(2)} market)`
|
||||||
|
: 'No market price available for this condition.';
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveFormPrice(form) {
|
||||||
|
// Returns a FormData ready to POST; % is converted to $ in-place.
|
||||||
|
const data = new FormData(form);
|
||||||
|
const mode = data.get('priceMode');
|
||||||
|
if (mode === 'percent') {
|
||||||
|
const condition = data.get('condition');
|
||||||
|
const prices = getMarketPrices(form);
|
||||||
|
const marketPrice = prices[condition] ?? 0;
|
||||||
|
const pct = parseFloat(data.get('purchasePrice')) || 0;
|
||||||
|
data.set('purchasePrice', ((pct / 100) * marketPrice).toFixed(2));
|
||||||
|
}
|
||||||
|
data.delete('priceMode'); // UI-only field
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Empty state helper ────────────────────────────────────────────────────
|
||||||
|
function syncEmptyState(invList) {
|
||||||
|
const emptyState = document.getElementById('inventoryEmptyState');
|
||||||
|
if (!emptyState) return;
|
||||||
|
const hasEntries = invList.querySelector('[data-inventory-id]') !== null;
|
||||||
|
emptyState.classList.toggle('d-none', hasEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Inventory form init (binding price-mode UI events) ───────────────────
|
||||||
function initInventoryForms(root = document) {
|
function initInventoryForms(root = document) {
|
||||||
// Fetch inventory entries for this card
|
// Fetch inventory entries for this card
|
||||||
const invList = root.querySelector('#inventoryEntryList') || document.getElementById('inventoryEntryList');
|
const invList = root.querySelector('#inventoryEntryList') || document.getElementById('inventoryEntryList');
|
||||||
@@ -58,7 +134,10 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
body.append('cardId', cardId);
|
body.append('cardId', cardId);
|
||||||
fetch('/api/inventory', { method: 'POST', body })
|
fetch('/api/inventory', { method: 'POST', body })
|
||||||
.then(r => r.text())
|
.then(r => r.text())
|
||||||
.then(html => { invList.innerHTML = html || ''; })
|
.then(html => {
|
||||||
|
invList.innerHTML = html || '';
|
||||||
|
syncEmptyState(invList);
|
||||||
|
})
|
||||||
.catch(() => { invList.innerHTML = '<span class="text-danger">Failed to load inventory</span>'; });
|
.catch(() => { invList.innerHTML = '<span class="text-danger">Failed to load inventory</span>'; });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -69,39 +148,38 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
if (form.dataset.inventoryBound === 'true') return;
|
if (form.dataset.inventoryBound === 'true') return;
|
||||||
form.dataset.inventoryBound = 'true';
|
form.dataset.inventoryBound = 'true';
|
||||||
|
|
||||||
const priceInput = form.querySelector('#purchasePrice');
|
const priceInput = form.querySelector('#purchasePrice');
|
||||||
const pricePrefix = form.querySelector('#pricePrefix');
|
const modeInputs = form.querySelectorAll('input[name="priceMode"]');
|
||||||
const priceSuffix = form.querySelector('#priceSuffix');
|
const condInputs = form.querySelectorAll('input[name="condition"]');
|
||||||
const priceHint = form.querySelector('#priceHint');
|
|
||||||
const modeInputs = form.querySelectorAll('input[name="priceMode"]');
|
|
||||||
|
|
||||||
if (!priceInput || !pricePrefix || !priceSuffix || !priceHint || !modeInputs.length) return;
|
// Set initial UI state
|
||||||
|
const checkedMode = form.querySelector('input[name="priceMode"]:checked')?.value ?? 'dollar';
|
||||||
function updatePriceMode(mode) {
|
applyPriceModeUI(form, checkedMode);
|
||||||
const isPct = mode === 'percent';
|
|
||||||
|
|
||||||
pricePrefix.classList.toggle('d-none', isPct);
|
|
||||||
priceSuffix.classList.toggle('d-none', !isPct);
|
|
||||||
|
|
||||||
priceInput.step = isPct ? '1' : '0.01';
|
|
||||||
priceInput.max = isPct ? '100' : '';
|
|
||||||
priceInput.placeholder = isPct ? '100' : '0.00';
|
|
||||||
priceInput.value = '';
|
|
||||||
priceHint.textContent = isPct
|
|
||||||
? 'Enter the percentage of market price you paid.'
|
|
||||||
: 'Enter the purchase price.';
|
|
||||||
|
|
||||||
// swap rounded edge classes based on visible prepend/append
|
|
||||||
priceInput.classList.toggle('rounded-end', !isPct);
|
|
||||||
priceInput.classList.toggle('rounded-start', isPct);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Mode toggle
|
||||||
modeInputs.forEach((input) => {
|
modeInputs.forEach((input) => {
|
||||||
input.addEventListener('change', () => updatePriceMode(input.value));
|
input.addEventListener('change', () => {
|
||||||
|
if (priceInput) priceInput.value = ''; // clear stale value on mode switch
|
||||||
|
applyPriceModeUI(form, input.value);
|
||||||
|
updatePriceHint(form);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const checked = form.querySelector('input[name="priceMode"]:checked');
|
// Condition change updates the hint when in % mode
|
||||||
updatePriceMode(checked ? checked.value : 'dollar');
|
condInputs.forEach((input) => {
|
||||||
|
input.addEventListener('change', () => updatePriceHint(form));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Live hint as user types
|
||||||
|
priceInput?.addEventListener('input', () => updatePriceHint(form));
|
||||||
|
|
||||||
|
// Reset — restore to $ mode
|
||||||
|
form.addEventListener('reset', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
applyPriceModeUI(form, 'dollar');
|
||||||
|
updatePriceHint(form);
|
||||||
|
}, 0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +554,8 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
const submitBtn = form.querySelector('button[type="submit"]');
|
const submitBtn = form.querySelector('button[type="submit"]');
|
||||||
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Saving…'; }
|
if (submitBtn) { submitBtn.disabled = true; submitBtn.textContent = 'Saving…'; }
|
||||||
|
|
||||||
const body = new FormData(form);
|
// resolveFormPrice converts % → $ and strips priceMode before POSTing
|
||||||
|
const body = resolveFormPrice(form);
|
||||||
body.append('action', 'add');
|
body.append('action', 'add');
|
||||||
body.append('cardId', cardId);
|
body.append('cardId', cardId);
|
||||||
|
|
||||||
@@ -484,9 +563,13 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
const res = await fetch('/api/inventory', { method: 'POST', body });
|
const res = await fetch('/api/inventory', { method: 'POST', body });
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const invList = document.getElementById('inventoryEntryList');
|
const invList = document.getElementById('inventoryEntryList');
|
||||||
if (invList) invList.innerHTML = html || '';
|
if (invList) {
|
||||||
|
invList.innerHTML = html || '';
|
||||||
|
syncEmptyState(invList);
|
||||||
|
}
|
||||||
form.reset();
|
form.reset();
|
||||||
form.classList.remove('was-validated');
|
form.classList.remove('was-validated');
|
||||||
|
// reset fires our listener which restores $ mode UI
|
||||||
} catch {
|
} catch {
|
||||||
// keep current inventory list state
|
// keep current inventory list state
|
||||||
} finally {
|
} finally {
|
||||||
@@ -538,7 +621,10 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
const res = await fetch('/api/inventory', { method: 'POST', body });
|
const res = await fetch('/api/inventory', { method: 'POST', body });
|
||||||
const html = await res.text();
|
const html = await res.text();
|
||||||
const invList = document.getElementById('inventoryEntryList');
|
const invList = document.getElementById('inventoryEntryList');
|
||||||
if (invList) invList.innerHTML = html || '';
|
if (invList) {
|
||||||
|
invList.innerHTML = html || '';
|
||||||
|
syncEmptyState(invList);
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// keep current state
|
// keep current state
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -132,8 +132,9 @@ export const inventory = pokeSchema.table('inventory',{
|
|||||||
cardId: integer().notNull(),
|
cardId: integer().notNull(),
|
||||||
condition: varchar({ length: 255 }).notNull(),
|
condition: varchar({ length: 255 }).notNull(),
|
||||||
quantity: integer(),
|
quantity: integer(),
|
||||||
purchasePrice: integer(),
|
purchasePrice: decimal({ precision: 10, scale: 2 }),
|
||||||
note: varchar({ length:255 })
|
note: varchar({ length:255 }),
|
||||||
|
createdAt: timestamp().notNull().defaultNow(),
|
||||||
},
|
},
|
||||||
(table) => [
|
(table) => [
|
||||||
index('idx_userid_cardid').on(table.userId, table.cardId)
|
index('idx_userid_cardid').on(table.userId, table.cardId)
|
||||||
|
|||||||
@@ -1,58 +1,84 @@
|
|||||||
import type { APIRoute } from 'astro';
|
import type { APIRoute } from 'astro';
|
||||||
import { db } from '../../db/index';
|
import { db } from '../../db/index';
|
||||||
import { inventory } from '../../db/schema';
|
import { inventory, skus, cards } from '../../db/schema';
|
||||||
import { client } from '../../db/typesense';
|
import { client } from '../../db/typesense';
|
||||||
import { eq } from 'drizzle-orm';
|
import { eq, and } 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-5 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-5 fw-semibold text-warning">-</div>';
|
||||||
if (pp > mp) return `<div class="fs-5 fw-semibold text-critical">-$${pp-mp}</div>`;
|
if (pp > mp) return `<div class="fs-5 fw-semibold text-critical">-$${(pp - mp).toFixed(2)}</div>`;
|
||||||
return `<div class="fs-5 fw-semibold text-success">+$${mp-pp}</div>`;
|
return `<div class="fs-6 fw-semibold text-success">+$${(mp - pp).toFixed(2)}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getInventory = async (userId: string, cardId: number) => {
|
||||||
|
|
||||||
const getInventory = async (userId:string, cardId:number) => {
|
const inventories = await db
|
||||||
|
.select({
|
||||||
const inventories = await db.query.inventory.findMany({
|
inventoryId: inventory.inventoryId,
|
||||||
where: { userId:userId, cardId:cardId, },
|
cardId: inventory.cardId,
|
||||||
with: { card: true, sku: true, }
|
condition: inventory.condition,
|
||||||
});
|
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)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(and(
|
||||||
|
eq(inventory.userId, userId),
|
||||||
|
eq(inventory.cardId, cardId)
|
||||||
|
));
|
||||||
|
|
||||||
const invHtml = inventories.map(inv => {
|
const invHtml = inventories.map(inv => {
|
||||||
|
const marketPrice = inv.marketPrice ? Number(inv.marketPrice).toFixed(2) : null;
|
||||||
|
const marketPriceDisplay = marketPrice ? `$${marketPrice}` : '—';
|
||||||
|
const purchasePriceDisplay = inv.purchasePrice ? `$${Number(inv.purchasePrice).toFixed(2)}` : '—';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<article class="border rounded-4 p-2 bg-body-tertiary inventory-entry-card"
|
<article class="alert alert-dark rounded-4 inventory-entry-card"
|
||||||
data-inventory-id="${inv.inventoryId}"
|
data-inventory-id="${inv.inventoryId}"
|
||||||
data-card-id="${inv.cardId}"
|
data-card-id="${inv.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 gap-2">
|
<div class="d-flex flex-column">
|
||||||
<!-- Top row -->
|
<!-- Top row -->
|
||||||
<div class="d-flex justify-content-between align-items-start 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-5 text-body mb-1">${inv.condition}</div>
|
<div class="fw-semibold fs-6 text-body mb-1">${inv.condition}</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="fs-7 text-secondary">Added: ${inv.createdAt ? new Date(inv.createdAt).toLocaleDateString() : '—'}</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Middle row -->
|
<!-- Middle row -->
|
||||||
<div class="row g-2">
|
<div class="row g-2">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="small text-secondary">Purchase price</div>
|
<div class="small text-secondary">Purchase price</div>
|
||||||
<div class="fs-5 fw-semibold">$${inv.purchasePrice}</div>
|
<div class="fs-6 fw-semibold">${purchasePriceDisplay}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="small text-secondary">Market price</div>
|
<div class="small text-secondary">Market price</div>
|
||||||
<div class="fs-5 text-success">$${inv.sku?.marketPrice}</div>
|
<div class="fs-6 text-success">${marketPriceDisplay}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<div class="small text-secondary">Gain / loss</div>
|
<div class="small text-secondary">Gain / loss</div>
|
||||||
${GainLoss(inv.purchasePrice, inv.sku?.marketPrice)}
|
${GainLoss(inv.purchasePrice, marketPrice)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Bottom row -->
|
<!-- Bottom row -->
|
||||||
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap">
|
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap mt-2">
|
||||||
<div class="d-flex align-items-center gap-2">
|
<div class="d-flex align-items-center gap-2">
|
||||||
<span class="small text-secondary">Qty</span>
|
<span class="small text-secondary">Qty</span>
|
||||||
<div class="btn-group" role="group" aria-label="Quantity controls">
|
<div class="btn-group" role="group" aria-label="Quantity controls">
|
||||||
@@ -63,7 +89,7 @@ const getInventory = async (userId:string, cardId:number) => {
|
|||||||
</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">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>
|
||||||
@@ -77,12 +103,10 @@ const getInventory = async (userId:string, cardId:number) => {
|
|||||||
headers: { 'Content-Type': 'text/html' },
|
headers: { 'Content-Type': 'text/html' },
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const addToInventory = async (userId:string, cardId:number, condition:string, purchasePrice:number, quantity:number, note:string, catalogName:string) => {
|
const addToInventory = async (userId: string, cardId: number, condition: string, 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,
|
cardId: cardId,
|
||||||
@@ -92,7 +116,6 @@ const addToInventory = async (userId:string, cardId:number, condition:string, pu
|
|||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
note: note,
|
note: note,
|
||||||
}).returning();
|
}).returning();
|
||||||
// And then add to Typesense
|
|
||||||
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,
|
||||||
@@ -101,23 +124,20 @@ const addToInventory = async (userId:string, cardId:number, condition:string, pu
|
|||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeFromInventory = async (inventoryId:string) => {
|
const removeFromInventory = async (inventoryId: string) => {
|
||||||
await db.delete(inventory).where(eq( inventory.inventoryId, inventoryId ));
|
await db.delete(inventory).where(eq(inventory.inventoryId, inventoryId));
|
||||||
await client.collections('inventories').documents(inventoryId).delete();
|
await client.collections('inventories').documents(inventoryId).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateInventory = async (inventoryId:string, quantity:number, purchasePrice:number, note:string) => {
|
const updateInventory = async (inventoryId: string, quantity: number, purchasePrice: number, note: string) => {
|
||||||
// Update in database
|
|
||||||
await db.update(inventory).set({
|
await db.update(inventory).set({
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
purchasePrice: purchasePrice,
|
purchasePrice: purchasePrice,
|
||||||
note: note,
|
note: note,
|
||||||
}).where(eq( inventory.inventoryId, inventoryId ));
|
}).where(eq(inventory.inventoryId, inventoryId));
|
||||||
// There is no need to update Typesense for these fields as they are not indexed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -132,7 +152,6 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|||||||
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, purchasePrice, quantity, note, catalogName);
|
await addToInventory(userId!, cardId, condition, purchasePrice, quantity, note, catalogName);
|
||||||
//return await getInventory(cardId);
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'remove':
|
case 'remove':
|
||||||
@@ -149,12 +168,8 @@ 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,6 +1,4 @@
|
|||||||
---
|
---
|
||||||
import { Show } from '@clerk/astro/components';
|
|
||||||
|
|
||||||
import ebay from "/vendors/ebay.svg?raw";
|
import ebay from "/vendors/ebay.svg?raw";
|
||||||
import SetIcon from '../../components/SetIcon.astro';
|
import SetIcon from '../../components/SetIcon.astro';
|
||||||
import EnergyIcon from '../../components/EnergyIcon.astro';
|
import EnergyIcon from '../../components/EnergyIcon.astro';
|
||||||
@@ -12,6 +10,18 @@ import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
|
|||||||
|
|
||||||
import { Tooltip } from "bootstrap";
|
import { Tooltip } from "bootstrap";
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
export const partial = true;
|
export const partial = true;
|
||||||
export const prerender = false;
|
export const prerender = false;
|
||||||
|
|
||||||
@@ -163,6 +173,14 @@ const conditionAttributes = (price: any) => {
|
|||||||
}[condition];
|
}[condition];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ── Build a market price lookup keyed by condition for use in JS ──────────
|
||||||
|
const marketPriceByCondition: Record<string, number> = {};
|
||||||
|
for (const price of card?.prices ?? []) {
|
||||||
|
if (price.condition && price.marketPrice != null) {
|
||||||
|
marketPriceByCondition[price.condition] = Number(price.marketPrice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const ebaySearchUrl = (card: any) => {
|
const ebaySearchUrl = (card: any) => {
|
||||||
return `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(card?.productUrlName)}+${encodeURIComponent(card?.set?.setUrlName)}+${encodeURIComponent(card?.number)}&LH_Sold=1&Graded=No&_dcat=183454`;
|
return `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(card?.productUrlName)}+${encodeURIComponent(card?.set?.setUrlName)}+${encodeURIComponent(card?.number)}&LH_Sold=1&Graded=No&_dcat=183454`;
|
||||||
};
|
};
|
||||||
@@ -251,13 +269,13 @@ 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 && (
|
||||||
<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>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="tab-content" id="myTabContent">
|
<div class="tab-content" id="myTabContent">
|
||||||
@@ -314,7 +332,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
|
|
||||||
<!-- Table only — chart is outside the tab panes -->
|
<!-- Table only — chart is outside the tab panes -->
|
||||||
<div class="w-100">
|
<div class="w-100">
|
||||||
<div class="alert alert-dark rounded p-2 mb-0 table-responsive">
|
<div class="alert alert-dark rounded p-2 mb-0 table-responsive d-none">
|
||||||
<h6>Latest Verified Sales</h6>
|
<h6>Latest Verified Sales</h6>
|
||||||
<table class="table table-sm mb-0">
|
<table class="table table-sm mb-0">
|
||||||
<caption class="small">Filtered to remove mismatched language variants</caption>
|
<caption class="small">Filtered to remove mismatched language variants</caption>
|
||||||
@@ -340,16 +358,16 @@ const altSearchUrl = (card: any) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
{hasAccess && (
|
||||||
<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-3">
|
<div class="row g-3">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<h5 class="mt-1 mb-2">Add {card?.productName} to inventory</h5>
|
<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 g-3">
|
<div class="row gx-3 gy-1">
|
||||||
<div class="col-4">
|
<div class="col-4">
|
||||||
<label for="quantity" class="form-label fw-medium">Quantity</label>
|
<label for="quantity" class="form-label">Quantity</label>
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
class="form-control mt-1"
|
class="form-control mt-1"
|
||||||
@@ -365,7 +383,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
|
|
||||||
<div class="col-8">
|
<div class="col-8">
|
||||||
<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 fw-medium">
|
<label for="purchasePrice" class="form-label">
|
||||||
Purchase price
|
Purchase price
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
@@ -414,7 +432,7 @@ const altSearchUrl = (card: any) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label class="form-label fw-medium">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 condition-input w-100" role="group" aria-label="Condition">
|
||||||
<input
|
<input
|
||||||
type="radio"
|
type="radio"
|
||||||
@@ -469,9 +487,8 @@ const altSearchUrl = (card: any) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="catalogName" class="form-label fw-medium">
|
<label for="catalogName" class="form-label">
|
||||||
Catalog
|
Catalog
|
||||||
<span class="text-body-tertiary fw-normal ms-1 small">optional</span>
|
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -491,9 +508,8 @@ const altSearchUrl = (card: any) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<label for="note" class="form-label fw-medium">
|
<label for="note" class="form-label">
|
||||||
Note
|
Note
|
||||||
<span class="text-body-tertiary fw-normal ms-1 small">optional</span>
|
|
||||||
</label>
|
</label>
|
||||||
<textarea
|
<textarea
|
||||||
class="form-control"
|
class="form-control"
|
||||||
@@ -515,10 +531,10 @@ const altSearchUrl = (card: any) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<h5 class="mt-1 mb-2">Inventory entries for {card?.productName}</h5>
|
<h6 class="mt-1 mb-2">Inventory entries for {card?.productName}</h6>
|
||||||
|
|
||||||
<!-- Empty state -->
|
<!-- Empty state -->
|
||||||
<div class="alert alert-dark border-0 rounded-4 d-none" id="inventoryEmptyState">
|
<div class="alert alert-dark rounded-4 d-none" id="inventoryEmptyState">
|
||||||
<div class="fw-medium mb-1">No inventory entries yet</div>
|
<div class="fw-medium mb-1">No inventory entries yet</div>
|
||||||
<div class="text-secondary small">
|
<div class="text-secondary small">
|
||||||
Once you add copies of this card, they'll show up here.
|
Once you add copies of this card, they'll show up here.
|
||||||
@@ -526,17 +542,17 @@ const altSearchUrl = (card: any) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Inventory list -->
|
<!-- Inventory list -->
|
||||||
<div class="d-flex flex-column gap-3" id="inventoryEntryList" data-card-id={cardId}>
|
<div class="d-flex flex-column gap-3" id="inventoryEntryList" data-card-id={cardId} data-market-prices={JSON.stringify(marketPriceByCondition)}>
|
||||||
<span>Loading...</span>
|
<span>Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart lives permanently outside tab-content so Bootstrap never hides it. -->
|
<!-- Chart lives permanently outside tab-content so Bootstrap never hides it. -->
|
||||||
<div class="d-block d-lg-flex gap-1 mt-1">
|
<div class="d-block d-lg-flex gap-1 mt-1 price-chart-container">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="alert alert-dark rounded p-2 mb-0">
|
<div class="alert alert-dark rounded p-2 mb-0">
|
||||||
<h6>Market Price History</h6>
|
<h6>Market Price History</h6>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
---
|
---
|
||||||
import { client } from '../../db/typesense';
|
import { client } from '../../db/typesense';
|
||||||
|
import { clerkClient } from '@clerk/astro/server';
|
||||||
|
|
||||||
import { Show } from '@clerk/astro/components';
|
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";
|
||||||
@@ -286,11 +295,11 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
|
|
||||||
{pokemon.map((card:any) => (
|
{pokemon.map((card:any) => (
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
{hasAccess && (
|
||||||
<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" 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>
|
<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'});">
|
||||||
<div class="image-grow rounded-4 card-image" data-energy={card.energyType} data-rarity={card.rarityName} data-variant={card.variant} data-name={card.productName}><img src={`/static/cards/${card.productId}.jpg`} alt={card.productName} id="cardImage" loading="lazy" decoding="async" class="img-fluid rounded-4 mb-2 w-100" onerror="this.onerror=null; this.src='/static/cards/default.jpg'; this.closest('.image-grow')?.setAttribute('data-default','true')"/><span class="position-absolute top-50 start-0 d-inline medium-icon"><FirstEditionIcon edition={card?.variant} /></span>
|
<div class="image-grow rounded-4 card-image" data-energy={card.energyType} data-rarity={card.rarityName} data-variant={card.variant} data-name={card.productName}><img src={`/static/cards/${card.productId}.jpg`} alt={card.productName} id="cardImage" loading="lazy" decoding="async" class="img-fluid rounded-4 mb-2 w-100" onerror="this.onerror=null; this.src='/static/cards/default.jpg'; this.closest('.image-grow')?.setAttribute('data-default','true')"/><span class="position-absolute top-50 start-0 d-inline medium-icon"><FirstEditionIcon edition={card?.variant} /></span>
|
||||||
<div class="holo-shine"></div>
|
<div class="holo-shine"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user