Compare commits
4 Commits
master
...
91823174d2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91823174d2 | ||
|
|
943bd33c9a | ||
|
|
9975db20cb | ||
|
|
db12844dea |
@@ -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
|
Misc UI
|
||||||
-------------------------------------------------- */
|
-------------------------------------------------- */
|
||||||
@@ -378,6 +412,18 @@ $tiers: (
|
|||||||
stroke: var(--bs-info-border-subtle);
|
stroke: var(--bs-info-border-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.delete-svg {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
fill: var(--bs-danger);
|
||||||
|
stroke: var(--bs-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover .delete-svg {
|
||||||
|
fill: var(--bs-danger-border-subtle);
|
||||||
|
stroke: var(--bs-danger-border-subtle);
|
||||||
|
}
|
||||||
|
|
||||||
.shadow-filter {
|
.shadow-filter {
|
||||||
filter:
|
filter:
|
||||||
drop-shadow(0 5px 5px rgba(0, 0, 0, 0.3))
|
drop-shadow(0 5px 5px rgba(0, 0, 0, 0.3))
|
||||||
@@ -418,20 +464,16 @@ $tiers: (
|
|||||||
}
|
}
|
||||||
|
|
||||||
.inventory-button {
|
.inventory-button {
|
||||||
width: 40px;
|
margin-bottom: -2.25rem;
|
||||||
height: 40px;
|
margin-right: -0.5rem;
|
||||||
margin-bottom: -2rem;
|
z-index: 2;
|
||||||
margin-right: -0.25rem;
|
|
||||||
border-radius: 0.33rem;
|
|
||||||
background-color: hsl(262, 47%, 55%);
|
background-color: hsl(262, 47%, 55%);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inventory-label {
|
.inventory-button:hover {
|
||||||
width: 100%;
|
background-color: hsl(262, 39%, 40%);
|
||||||
height: 100%;
|
color: #fff;
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fs-7 {
|
.fs-7 {
|
||||||
|
|||||||
@@ -2,51 +2,97 @@ import * as bootstrap from 'bootstrap';
|
|||||||
window.bootstrap = bootstrap;
|
window.bootstrap = bootstrap;
|
||||||
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
|
||||||
|
|
||||||
// trap browser back and close the modal if open
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Initialize all Bootstrap modals
|
||||||
|
document.querySelectorAll('.modal').forEach(modalEl => {
|
||||||
|
bootstrap.Modal.getOrCreateInstance(modalEl);
|
||||||
|
});
|
||||||
|
|
||||||
import { Tooltip } from "bootstrap";
|
// Initialize tooltips
|
||||||
|
|
||||||
// Initialize all tooltips globally
|
|
||||||
const initTooltips = () => {
|
|
||||||
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => {
|
||||||
if (!el._tooltipInstance) {
|
if (!el._tooltipInstance) {
|
||||||
el._tooltipInstance = new Tooltip(el, {
|
el._tooltipInstance = new bootstrap.Tooltip(el, { container: 'body' });
|
||||||
container: 'body', // ensures tooltip is appended to body, important for modals
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
||||||
// Run on page load
|
// ---------------- DASHBOARD LOGIC ----------------
|
||||||
if (document.readyState === 'loading') {
|
const toggleBtn = document.getElementById("toggleViewBtn");
|
||||||
document.addEventListener('DOMContentLoaded', initTooltips);
|
const gridView = document.getElementById("gridView");
|
||||||
|
const tableView = document.getElementById("tableView");
|
||||||
|
const searchInput = document.getElementById("inventorySearch");
|
||||||
|
const tbody = document.getElementById("inventoryRows");
|
||||||
|
|
||||||
|
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 {
|
} else {
|
||||||
initTooltips();
|
gridView.style.display = "block";
|
||||||
|
tableView.style.display = "none";
|
||||||
|
toggleBtn.textContent = "Switch to Table View";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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";
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Optional: observe DOM changes for dynamically added tooltips (e.g., modals loaded later)
|
// SORTING
|
||||||
const observer = new MutationObserver(() => initTooltips());
|
document.querySelectorAll("th[data-key]").forEach(th => {
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
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";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -32,6 +32,12 @@ function setEmptyState(isEmpty) {
|
|||||||
canvasWrapper.classList.toggle('d-none', isEmpty);
|
canvasWrapper.classList.toggle('d-none', isEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setChartVisible(visible) {
|
||||||
|
const modal = document.getElementById('cardModal');
|
||||||
|
const chartWrapper = modal?.querySelector('#priceHistoryChart')?.closest('.alert');
|
||||||
|
if (chartWrapper) chartWrapper.classList.toggle('d-none', !visible);
|
||||||
|
}
|
||||||
|
|
||||||
function buildChartData(history, rangeKey) {
|
function buildChartData(history, rangeKey) {
|
||||||
const cutoff = RANGE_DAYS[rangeKey] === Infinity
|
const cutoff = RANGE_DAYS[rangeKey] === Infinity
|
||||||
? new Date(0)
|
? new Date(0)
|
||||||
@@ -39,20 +45,14 @@ function buildChartData(history, rangeKey) {
|
|||||||
|
|
||||||
const filtered = history.filter(r => new Date(r.calculatedAt) >= cutoff);
|
const filtered = history.filter(r => new Date(r.calculatedAt) >= cutoff);
|
||||||
|
|
||||||
// Always build the full date axis for the selected window, even if sparse.
|
|
||||||
// Generate one label per day in the range so the x-axis reflects the
|
|
||||||
// chosen period rather than collapsing to only the days that have data.
|
|
||||||
const dataDateSet = new Set(filtered.map(r => r.calculatedAt));
|
const dataDateSet = new Set(filtered.map(r => r.calculatedAt));
|
||||||
const allDates = [...dataDateSet].sort((a, b) => new Date(a) - new Date(b));
|
const allDates = [...dataDateSet].sort((a, b) => new Date(a) - new Date(b));
|
||||||
|
|
||||||
// If we have real data, expand the axis to span from cutoff → today so
|
|
||||||
// empty stretches at the start/end of a range are visible.
|
|
||||||
let axisLabels = allDates;
|
let axisLabels = allDates;
|
||||||
if (allDates.length > 0 && RANGE_DAYS[rangeKey] !== Infinity) {
|
if (allDates.length > 0 && RANGE_DAYS[rangeKey] !== Infinity) {
|
||||||
const start = new Date(cutoff);
|
const start = new Date(cutoff);
|
||||||
const end = new Date();
|
const end = new Date();
|
||||||
const expanded = [];
|
const expanded = [];
|
||||||
// Step through every day in the window
|
|
||||||
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
|
||||||
expanded.push(d.toISOString().split('T')[0]);
|
expanded.push(d.toISOString().split('T')[0]);
|
||||||
}
|
}
|
||||||
@@ -101,17 +101,9 @@ function buildChartData(history, rangeKey) {
|
|||||||
function updateChart() {
|
function updateChart() {
|
||||||
if (!chartInstance) return;
|
if (!chartInstance) return;
|
||||||
const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange);
|
const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange);
|
||||||
|
|
||||||
// Always push the new labels/datasets to the chart so the x-axis
|
|
||||||
// reflects the selected time window — even when there's no data for
|
|
||||||
// the active condition. Then toggle the empty state overlay on top.
|
|
||||||
chartInstance.data.labels = labels;
|
chartInstance.data.labels = labels;
|
||||||
chartInstance.data.datasets = datasets;
|
chartInstance.data.datasets = datasets;
|
||||||
chartInstance.update('none');
|
chartInstance.update('none');
|
||||||
|
|
||||||
// Show the empty state overlay if the active condition has no points
|
|
||||||
// in this window, but leave the (empty) chart visible underneath so
|
|
||||||
// the axis communicates the selected period.
|
|
||||||
setEmptyState(!hasData || !activeConditionHasData);
|
setEmptyState(!hasData || !activeConditionHasData);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,7 +127,6 @@ function initPriceChart(canvas) {
|
|||||||
|
|
||||||
const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange);
|
const { labels, datasets, hasData, activeConditionHasData } = buildChartData(allHistory, activeRange);
|
||||||
|
|
||||||
// Render the chart regardless — show empty state overlay if needed
|
|
||||||
setEmptyState(!hasData || !activeConditionHasData);
|
setEmptyState(!hasData || !activeConditionHasData);
|
||||||
|
|
||||||
chartInstance = new Chart(canvas.getContext('2d'), {
|
chartInstance = new Chart(canvas.getContext('2d'), {
|
||||||
@@ -202,9 +193,16 @@ function initFromCanvas(canvas) {
|
|||||||
activeCondition = "Near Mint";
|
activeCondition = "Near Mint";
|
||||||
activeRange = '1m';
|
activeRange = '1m';
|
||||||
const modal = document.getElementById('cardModal');
|
const modal = document.getElementById('cardModal');
|
||||||
|
|
||||||
modal?.querySelectorAll('.price-range-btn').forEach(b => {
|
modal?.querySelectorAll('.price-range-btn').forEach(b => {
|
||||||
b.classList.toggle('active', b.dataset.range === '1m');
|
b.classList.toggle('active', b.dataset.range === '1m');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Hide chart if the vendor tab is already active when the modal opens
|
||||||
|
// (e.g. opened via the inventory button)
|
||||||
|
const activeTab = modal?.querySelector('.nav-link.active')?.getAttribute('data-bs-target');
|
||||||
|
setChartVisible(activeTab !== '#nav-vendor');
|
||||||
|
|
||||||
initPriceChart(canvas);
|
initPriceChart(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,6 +223,10 @@ function setup() {
|
|||||||
document.addEventListener('shown.bs.tab', (e) => {
|
document.addEventListener('shown.bs.tab', (e) => {
|
||||||
if (!modal.contains(e.target)) return;
|
if (!modal.contains(e.target)) return;
|
||||||
const target = e.target?.getAttribute('data-bs-target');
|
const target = e.target?.getAttribute('data-bs-target');
|
||||||
|
|
||||||
|
// Hide the chart when the vendor tab is active, show it for all others
|
||||||
|
setChartVisible(target !== '#nav-vendor');
|
||||||
|
|
||||||
const conditionMap = {
|
const conditionMap = {
|
||||||
'#nav-nm': 'Near Mint',
|
'#nav-nm': 'Near Mint',
|
||||||
'#nav-lp': 'Lightly Played',
|
'#nav-lp': 'Lightly Played',
|
||||||
|
|||||||
@@ -44,15 +44,54 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
|
|
||||||
<BackToTop />
|
<BackToTop />
|
||||||
|
|
||||||
<!-- <script src="src/assets/js/holofoil-init.js" is:inline></script> -->
|
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
(function () {
|
(function () {
|
||||||
|
|
||||||
|
function initInventoryForms(root = document) {
|
||||||
|
const forms = root.querySelectorAll('[data-inventory-form]');
|
||||||
|
|
||||||
|
forms.forEach((form) => {
|
||||||
|
if (form.dataset.inventoryBound === 'true') return;
|
||||||
|
form.dataset.inventoryBound = 'true';
|
||||||
|
|
||||||
|
const priceInput = form.querySelector('#purchasePrice');
|
||||||
|
const pricePrefix = form.querySelector('#pricePrefix');
|
||||||
|
const priceSuffix = form.querySelector('#priceSuffix');
|
||||||
|
const priceHint = form.querySelector('#priceHint');
|
||||||
|
const modeInputs = form.querySelectorAll('input[name="priceMode"]');
|
||||||
|
|
||||||
|
if (!priceInput || !pricePrefix || !priceSuffix || !priceHint || !modeInputs.length) return;
|
||||||
|
|
||||||
|
function updatePriceMode(mode) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
modeInputs.forEach((input) => {
|
||||||
|
input.addEventListener('change', () => updatePriceMode(input.value));
|
||||||
|
});
|
||||||
|
|
||||||
|
const checked = form.querySelector('input[name="priceMode"]:checked');
|
||||||
|
updatePriceMode(checked ? checked.value : 'dollar');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── Sort dropdown ─────────────────────────────────────────────────────────
|
// ── Sort dropdown ─────────────────────────────────────────────────────────
|
||||||
document.addEventListener('click', (e) => {
|
document.addEventListener('click', (e) => {
|
||||||
const sortBy = document.getElementById('sortBy');
|
|
||||||
|
|
||||||
const btn = e.target.closest('#sortBy [data-bs-toggle="dropdown"]');
|
const btn = e.target.closest('#sortBy [data-bs-toggle="dropdown"]');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -118,7 +157,6 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
canvas.width = img.naturalWidth;
|
canvas.width = img.naturalWidth;
|
||||||
canvas.height = img.naturalHeight;
|
canvas.height = img.naturalHeight;
|
||||||
|
|
||||||
// Load with crossOrigin so toBlob() stays untainted
|
|
||||||
await new Promise((resolve) => {
|
await new Promise((resolve) => {
|
||||||
const clean = new Image();
|
const clean = new Image();
|
||||||
clean.crossOrigin = 'anonymous';
|
clean.crossOrigin = 'anonymous';
|
||||||
@@ -172,6 +210,20 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Tab switching helper ──────────────────────────────────────────────────
|
||||||
|
function switchToRequestedTab() {
|
||||||
|
const tab = sessionStorage.getItem('openModalTab');
|
||||||
|
if (!tab) return;
|
||||||
|
sessionStorage.removeItem('openModalTab');
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
try {
|
||||||
|
const tabEl = document.querySelector(`#cardModal [data-bs-target="#${tab}"]`);
|
||||||
|
if (tabEl) bootstrap.Tab.getOrCreateInstance(tabEl).show();
|
||||||
|
} catch (e) {
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// ── State ─────────────────────────────────────────────────────────────────
|
// ── State ─────────────────────────────────────────────────────────────────
|
||||||
const cardIndex = [];
|
const cardIndex = [];
|
||||||
let currentCardId = null;
|
let currentCardId = null;
|
||||||
@@ -259,10 +311,17 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
|
|
||||||
if (modal._reconnectChartObserver) modal._reconnectChartObserver();
|
if (modal._reconnectChartObserver) modal._reconnectChartObserver();
|
||||||
|
|
||||||
|
modal.querySelectorAll('[data-bs-toggle="tab"]').forEach(el => {
|
||||||
|
bootstrap.Tab.getInstance(el)?.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
modal.innerHTML = html;
|
modal.innerHTML = html;
|
||||||
|
|
||||||
if (typeof htmx !== 'undefined') htmx.process(modal);
|
if (typeof htmx !== 'undefined') htmx.process(modal);
|
||||||
|
initInventoryForms(modal);
|
||||||
updateNavButtons(modal);
|
updateNavButtons(modal);
|
||||||
initChartAfterSwap(modal);
|
initChartAfterSwap(modal);
|
||||||
|
switchToRequestedTab();
|
||||||
};
|
};
|
||||||
|
|
||||||
if (document.startViewTransition && direction) {
|
if (document.startViewTransition && direction) {
|
||||||
@@ -347,8 +406,14 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
|
|
||||||
if (target._reconnectChartObserver) target._reconnectChartObserver();
|
if (target._reconnectChartObserver) target._reconnectChartObserver();
|
||||||
|
|
||||||
|
target.querySelectorAll('[data-bs-toggle="tab"]').forEach(el => {
|
||||||
|
bootstrap.Tab.getInstance(el)?.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
target.innerHTML = html;
|
target.innerHTML = html;
|
||||||
|
|
||||||
if (typeof htmx !== 'undefined') htmx.process(target);
|
if (typeof htmx !== 'undefined') htmx.process(target);
|
||||||
|
initInventoryForms(target);
|
||||||
|
|
||||||
const destImg = target.querySelector('img.card-image');
|
const destImg = target.querySelector('img.card-image');
|
||||||
if (destImg) {
|
if (destImg) {
|
||||||
@@ -365,6 +430,7 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
await transition.finished;
|
await transition.finished;
|
||||||
updateNavButtons(target);
|
updateNavButtons(target);
|
||||||
initChartAfterSwap(target);
|
initChartAfterSwap(target);
|
||||||
|
switchToRequestedTab();
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[card-modal] transition failed:', err);
|
console.error('[card-modal] transition failed:', err);
|
||||||
@@ -383,10 +449,18 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
cardModal.addEventListener('shown.bs.modal', () => {
|
cardModal.addEventListener('shown.bs.modal', () => {
|
||||||
updateNavButtons(cardModal);
|
updateNavButtons(cardModal);
|
||||||
initChartAfterSwap(cardModal);
|
initChartAfterSwap(cardModal);
|
||||||
|
initInventoryForms(cardModal);
|
||||||
|
switchToRequestedTab();
|
||||||
});
|
});
|
||||||
|
|
||||||
cardModal.addEventListener('hidden.bs.modal', () => {
|
cardModal.addEventListener('hidden.bs.modal', () => {
|
||||||
currentCardId = null;
|
currentCardId = null;
|
||||||
updateNavButtons(null);
|
updateNavButtons(null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
initInventoryForms();
|
||||||
|
});
|
||||||
|
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
37
src/components/InventoryTable.astro
Normal file
37
src/components/InventoryTable.astro
Normal file
@@ -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 },
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-dark table-striped table-hover align-middle">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Card</th>
|
||||||
|
<th>Set</th>
|
||||||
|
<th>Condition</th>
|
||||||
|
<th>Qty</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th>Market</th>
|
||||||
|
<th>Gain/Loss</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{mockInventory.map(card => (
|
||||||
|
<tr>
|
||||||
|
<td>{card.name}</td>
|
||||||
|
<td>{card.set}</td>
|
||||||
|
<td>{card.condition}</td>
|
||||||
|
<td>{card.qty}</td>
|
||||||
|
<td>${card.price}</td>
|
||||||
|
<td>${card.market}</td>
|
||||||
|
<td class={card.gain >= 0 ? "text-success" : "text-danger"}>
|
||||||
|
{card.gain >= 0 ? "+" : "-"}${Math.abs(card.gain)}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
1185
src/pages/dashboard.astro
Normal file
1185
src/pages/dashboard.astro
Normal file
File diff suppressed because it is too large
Load Diff
@@ -226,31 +226,31 @@ const altSearchUrl = (card: any) => {
|
|||||||
<ul class="nav nav-tabs nav-fill border-0 me-3 mb-2" id="myTab" role="tablist">
|
<ul class="nav nav-tabs nav-fill border-0 me-3 mb-2" id="myTab" role="tablist">
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link nm active" id="nm-tab" data-bs-toggle="tab" data-bs-target="#nav-nm" type="button" role="tab" aria-controls="nav-nm" aria-selected="true">
|
<button class="nav-link nm active" id="nm-tab" data-bs-toggle="tab" data-bs-target="#nav-nm" type="button" role="tab" aria-controls="nav-nm" aria-selected="true">
|
||||||
<span class="d-none d-xxl-inline">Near Mint</span><span class="d-xxl-none">NM</span>
|
<span class="d-none">Near Mint</span><span class="d-inline">NM</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link lp" id="lp-tab" data-bs-toggle="tab" data-bs-target="#nav-lp" type="button" role="tab" aria-controls="nav-lp" aria-selected="false">
|
<button class="nav-link lp" id="lp-tab" data-bs-toggle="tab" data-bs-target="#nav-lp" type="button" role="tab" aria-controls="nav-lp" aria-selected="false">
|
||||||
<span class="d-none d-xxl-inline">Lightly Played</span><span class="d-xxl-none">LP</span>
|
<span class="d-none">Lightly Played</span><span class="d-inline">LP</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link mp" id="mp-tab" data-bs-toggle="tab" data-bs-target="#nav-mp" type="button" role="tab" aria-controls="nav-mp" aria-selected="false">
|
<button class="nav-link mp" id="mp-tab" data-bs-toggle="tab" data-bs-target="#nav-mp" type="button" role="tab" aria-controls="nav-mp" aria-selected="false">
|
||||||
<span class="d-none d-xxl-inline">Moderately Played</span><span class="d-xxl-none">MP</span>
|
<span class="d-none">Moderately Played</span><span class="d-inline">MP</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link hp" id="hp-tab" data-bs-toggle="tab" data-bs-target="#nav-hp" type="button" role="tab" aria-controls="nav-hp" aria-selected="false">
|
<button class="nav-link hp" id="hp-tab" data-bs-toggle="tab" data-bs-target="#nav-hp" type="button" role="tab" aria-controls="nav-hp" aria-selected="false">
|
||||||
<span class="d-none d-xxl-inline">Heavily Played</span><span class="d-xxl-none">HP</span>
|
<span class="d-none">Heavily Played</span><span class="d-inline">HP</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link dmg" id="dmg-tab" data-bs-toggle="tab" data-bs-target="#nav-dmg" type="button" role="tab" aria-controls="nav-dmg" aria-selected="false">
|
<button class="nav-link dmg" id="dmg-tab" data-bs-toggle="tab" data-bs-target="#nav-dmg" type="button" role="tab" aria-controls="nav-dmg" aria-selected="false">
|
||||||
<span class="d-none d-xxl-inline">Damaged</span><span class="d-xxl-none">DMG</span>
|
<span class="d-none">Damaged</span><span class="d-inline">DMG</span>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<button class="nav-link vendor d-none" 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>
|
||||||
@@ -337,7 +337,267 @@ const altSearchUrl = (card: any) => {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
<div class="tab-pane fade" id="nav-vendor" role="tabpanel" aria-labelledby="nav-vendor" tabindex="0"></div>
|
<div class="tab-pane fade" id="nav-vendor" role="tabpanel" aria-labelledby="nav-vendor" tabindex="0">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<h5 class="my-3">Add {card?.productName} to inventory</h5>
|
||||||
|
|
||||||
|
<form id="inventoryForm" data-inventory-form novalidate>
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-4">
|
||||||
|
<label for="quantity" class="form-label fw-medium">Quantity</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control mt-1"
|
||||||
|
id="quantity"
|
||||||
|
name="quantity"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
value="1"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<div class="invalid-feedback">Required.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-8">
|
||||||
|
<div class="d-flex justify-content-between align-items-start gap-2 flex-wrap">
|
||||||
|
<label for="purchasePrice" class="form-label fw-medium mb-0 mt-1">
|
||||||
|
Purchase price
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="btn-group btn-group-sm price-toggle" role="group" aria-label="Price mode">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="priceMode"
|
||||||
|
id="mode-dollar"
|
||||||
|
value="dollar"
|
||||||
|
autocomplete="off"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-secondary" for="mode-dollar">$</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="priceMode"
|
||||||
|
id="mode-percent"
|
||||||
|
value="percent"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-outline-secondary" for="mode-percent">%</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="input-group">
|
||||||
|
<span class="input-group-text mt-1" id="pricePrefix">$</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control mt-1 rounded-end"
|
||||||
|
id="purchasePrice"
|
||||||
|
name="purchasePrice"
|
||||||
|
min="0"
|
||||||
|
step="0.01"
|
||||||
|
placeholder="0.00"
|
||||||
|
aria-describedby="pricePrefix priceSuffix priceHint"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<span class="input-group-text d-none mt-1" id="priceSuffix">%</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-text" id="priceHint">Enter the purchase price.</div>
|
||||||
|
<div class="invalid-feedback">Enter a purchase price.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label class="form-label fw-medium">Condition</label>
|
||||||
|
<div class="btn-group condition-input w-100" role="group" aria-label="Condition">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="condition"
|
||||||
|
id="cond-nm"
|
||||||
|
value="Near Mint"
|
||||||
|
autocomplete="off"
|
||||||
|
checked
|
||||||
|
/>
|
||||||
|
<label class="btn btn-cond-nm" for="cond-nm">NM</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="condition"
|
||||||
|
id="cond-lp"
|
||||||
|
value="Lightly Played"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-cond-lp" for="cond-lp">LP</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="condition"
|
||||||
|
id="cond-mp"
|
||||||
|
value="Moderately Played"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-cond-mp" for="cond-mp">MP</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="condition"
|
||||||
|
id="cond-hp"
|
||||||
|
value="Heavily Played"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-cond-hp" for="cond-hp">HP</label>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
class="btn-check"
|
||||||
|
name="condition"
|
||||||
|
id="cond-dmg"
|
||||||
|
value="Damaged"
|
||||||
|
autocomplete="off"
|
||||||
|
/>
|
||||||
|
<label class="btn btn-cond-dmg" for="cond-dmg">DMG</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="note" class="form-label fw-medium">
|
||||||
|
Note
|
||||||
|
<span class="text-body-tertiary fw-normal ms-1 small">optional</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
class="form-control"
|
||||||
|
id="note"
|
||||||
|
name="note"
|
||||||
|
rows="2"
|
||||||
|
maxlength="255"
|
||||||
|
placeholder="e.g. bought at local shop, gift, graded copy…"
|
||||||
|
></textarea>
|
||||||
|
<div class="form-text text-end" id="noteCount">0 / 255</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 d-flex gap-3 pt-2">
|
||||||
|
<button type="reset" class="btn btn-outline-danger flex-fill">Reset</button>
|
||||||
|
<button type="submit" class="btn btn-success flex-fill">Save to inventory</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<h5 class="my-3">Inventory entries for {card?.productName}</h5>
|
||||||
|
|
||||||
|
<!-- Empty state -->
|
||||||
|
<div class="alert alert-dark border-0 rounded-4 d-none" id="inventoryEmptyState">
|
||||||
|
<div class="fw-medium mb-1">No inventory entries yet</div>
|
||||||
|
<div class="text-secondary small">
|
||||||
|
Once you add copies of this card, they’ll show up here.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Inventory list -->
|
||||||
|
<div class="d-flex flex-column gap-3" id="inventoryEntryList">
|
||||||
|
|
||||||
|
<!-- Inventory card -->
|
||||||
|
<article class="border rounded-4 p-2 bg-body-tertiary inventory-entry-card">
|
||||||
|
<div class="d-flex flex-column gap-2">
|
||||||
|
|
||||||
|
<!-- Top row -->
|
||||||
|
<div class="d-flex justify-content-between align-items-start gap-3">
|
||||||
|
<div class="min-w-0 flex-grow-1">
|
||||||
|
<div class="fw-semibold fs-5 text-body mb-1">Near Mint</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Middle row -->
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="small text-secondary">Purchase price</div>
|
||||||
|
<div class="fs-5 fw-semibold">$8.50</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="small text-secondary">Market price</div>
|
||||||
|
<div class="fs-5 text-success">$10.25</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="small text-secondary">Gain / loss</div>
|
||||||
|
<div class="fs-5 fw-semibold text-success">+$1.75</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Bottom row -->
|
||||||
|
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="small text-secondary">Qty</span>
|
||||||
|
|
||||||
|
<div class="btn-group" role="group" aria-label="Quantity controls">
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm">−</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" tabindex="-1">2</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<!-- Inventory card -->
|
||||||
|
<article class="border rounded-4 p-2 bg-body-tertiary inventory-entry-card">
|
||||||
|
<div class="d-flex flex-column gap-2">
|
||||||
|
|
||||||
|
<!-- Top row -->
|
||||||
|
<div class="d-flex justify-content-between align-items-start gap-3">
|
||||||
|
<div class="min-w-0 flex-grow-1">
|
||||||
|
<div class="fw-semibold fs-5 text-body mb-1">Lightly Played</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="small text-secondary">Purchase price</div>
|
||||||
|
<div class="fs-5 fw-semibold">$6.00</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="small text-secondary">Market price</div>
|
||||||
|
<div class="fs-5 text-success">$8.00</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="small text-secondary">Gain / loss</div>
|
||||||
|
<div class="fs-5 fw-semibold text-success">+$4.00</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center gap-3 flex-wrap">
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="small text-secondary">Qty</span>
|
||||||
|
<div class="btn-group" role="group" aria-label="Quantity controls">
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm">−</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm" tabindex="-1">2</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary btn-sm">+</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex align-items-center gap-2 flex-wrap">
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-secondary">Edit</button>
|
||||||
|
<button type="button" class="btn btn-sm btn-outline-danger">Remove</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</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. -->
|
||||||
|
|||||||
@@ -283,9 +283,9 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
|
|
||||||
{pokemon.map((card:any) => (
|
{pokemon.map((card:any) => (
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="inventory-button position-relative float-end shadow-filter text-center d-none">
|
<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');">
|
||||||
<div class="inventory-label pt-2">+/-</div>
|
<b>+/–</b>
|
||||||
</div>
|
</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