inventory dashboard setup

This commit is contained in:
Zach Harding
2026-03-25 08:42:17 -04:00
parent db12844dea
commit 9975db20cb
6 changed files with 1657 additions and 191 deletions

View File

@@ -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;
}

View File

@@ -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 });
});

View File

@@ -44,15 +44,54 @@ import BackToTop from "./BackToTop.astro"
<BackToTop />
<!-- <script src="src/assets/js/holofoil-init.js" is:inline></script> -->
<script is:inline>
(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 ─────────────────────────────────────────────────────────
document.addEventListener('click', (e) => {
const sortBy = document.getElementById('sortBy');
const btn = e.target.closest('#sortBy [data-bs-toggle="dropdown"]');
if (btn) {
e.preventDefault();
@@ -172,15 +211,16 @@ import BackToTop from "./BackToTop.astro"
}
// ── Tab switching helper ──────────────────────────────────────────────────
// Called after every modal swap. Checks sessionStorage for a tab request
// set by the inventory button click, activates it once, then clears it.
function switchToRequestedTab() {
const tab = sessionStorage.getItem('openModalTab');
if (!tab) return;
sessionStorage.removeItem('openModalTab');
requestAnimationFrame(() => {
const tabEl = document.querySelector(`#cardModal [data-bs-target="#${tab}"]`);
if (tabEl) bootstrap.Tab.getOrCreateInstance(tabEl).show();
try {
const tabEl = document.querySelector(`#cardModal [data-bs-target="#${tab}"]`);
if (tabEl) bootstrap.Tab.getOrCreateInstance(tabEl).show();
} catch (e) {
}
});
}
@@ -271,8 +311,14 @@ import BackToTop from "./BackToTop.astro"
if (modal._reconnectChartObserver) modal._reconnectChartObserver();
modal.querySelectorAll('[data-bs-toggle="tab"]').forEach(el => {
bootstrap.Tab.getInstance(el)?.dispose();
});
modal.innerHTML = html;
if (typeof htmx !== 'undefined') htmx.process(modal);
initInventoryForms(modal);
updateNavButtons(modal);
initChartAfterSwap(modal);
switchToRequestedTab();
@@ -360,8 +406,14 @@ import BackToTop from "./BackToTop.astro"
if (target._reconnectChartObserver) target._reconnectChartObserver();
target.querySelectorAll('[data-bs-toggle="tab"]').forEach(el => {
bootstrap.Tab.getInstance(el)?.dispose();
});
target.innerHTML = html;
if (typeof htmx !== 'undefined') htmx.process(target);
initInventoryForms(target);
const destImg = target.querySelector('img.card-image');
if (destImg) {
@@ -397,12 +449,18 @@ import BackToTop from "./BackToTop.astro"
cardModal.addEventListener('shown.bs.modal', () => {
updateNavButtons(cardModal);
initChartAfterSwap(cardModal);
initInventoryForms(cardModal);
switchToRequestedTab();
});
cardModal.addEventListener('hidden.bs.modal', () => {
currentCardId = null;
updateNavButtons(null);
});
document.addEventListener('DOMContentLoaded', () => {
initInventoryForms();
});
})();
</script>

View 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>

1189
src/pages/dashboard.astro Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -338,157 +338,265 @@ const altSearchUrl = (card: any) => {
})}
<div class="tab-pane fade" id="nav-vendor" role="tabpanel" aria-labelledby="nav-vendor" tabindex="0">
<style>
:root {
--c-nm: 156, 204, 102;
--c-lp: 211, 225, 86;
--c-mp: 255, 238, 87;
--c-hp: 255, 201, 41;
--c-dmg: 255, 167, 36;
}
<div class="row g-3">
<div class="col-12 col-md-6">
<h5 class="my-3">Add {card?.productName} to inventory</h5>
.btn-check:checked + .btn-cond-nm { background: rgba(var(--c-nm), 1); border-color: rgba(var(--c-nm), 1); color: #2d4a10; }
.btn-check:checked + .btn-cond-lp { background: rgba(var(--c-lp), 1); border-color: rgba(var(--c-lp), 1); color: #3a4310; }
.btn-check:checked + .btn-cond-mp { background: rgba(var(--c-mp), 1); border-color: rgba(var(--c-mp), 1); color: #44420a; }
.btn-check:checked + .btn-cond-hp { background: rgba(var(--c-hp), 1); border-color: rgba(var(--c-hp), 1); color: #4a3608; }
.btn-check:checked + .btn-cond-dmg { background: rgba(var(--c-dmg), 1); border-color: rgba(var(--c-dmg), 1); color: #4a2c08; }
<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>
.btn-cond-nm, .btn-cond-lp, .btn-cond-mp, .btn-cond-hp, .btn-cond-dmg {
border: 1px solid rgba(255,255,255,0.15);
color: var(--bs-body-color);
background: transparent;
font-size: 0.8rem;
font-weight: 500;
transition: background 0.1s, border-color 0.1s;
}
.btn-cond-nm:hover { background: rgba(var(--c-nm), 0.2); border-color: rgba(var(--c-nm), 0.6); }
.btn-cond-lp:hover { background: rgba(var(--c-lp), 0.2); border-color: rgba(var(--c-lp), 0.6); }
.btn-cond-mp:hover { background: rgba(var(--c-mp), 0.2); border-color: rgba(var(--c-mp), 0.6); }
.btn-cond-hp:hover { background: rgba(var(--c-hp), 0.2); border-color: rgba(var(--c-hp), 0.6); }
.btn-cond-dmg:hover { background: rgba(var(--c-dmg), 0.2); border-color: rgba(var(--c-dmg), 0.6); }
<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>
.price-toggle .btn { font-size: 0.75rem; padding: 0.25rem 0.6rem; line-height: 1; }
</style>
<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>
<form id="inventoryForm" novalidate>
<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="row g-3 mb-3">
<div class="col-4">
<label for="quantity" class="form-label fw-medium">Quantity</label>
<input type="number" class="form-control" id="quantity" name="quantity"
min="1" step="1" value="1" required>
<div class="invalid-feedback">Required.</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="col-8">
<label class="form-label fw-medium">Condition</label>
<div class="btn-group 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>
<div class="form-text" id="priceHint">Enter the purchase price.</div>
<div class="invalid-feedback">Enter a purchase price.</div>
</div>
<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>
<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-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-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-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-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-dmg" value="Damaged" autocomplete="off">
<label class="btn btn-cond-dmg" for="cond-dmg">DMG</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, theyll show up here.
</div>
</div>
</div>
<div class="mb-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<label for="purchasePrice" class="form-label fw-medium mb-0">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">$ amount</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">% of market</label>
</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 class="input-group">
<span class="input-group-text" id="pricePrefix">$</span>
<input type="number" class="form-control" 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" id="priceSuffix">%</span>
</div>
<div class="form-text" id="priceHint">Enter the amount you paid.</div>
<div class="invalid-feedback">Enter a purchase price.</div>
</div>
<div class="mb-4">
<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="d-flex gap-2">
<button type="submit" class="btn btn-success flex-fill">Save to inventory</button>
<button type="reset" class="btn btn-outline-secondary">Reset</button>
</div>
</form>
<script>
(function () {
const priceInput = document.getElementById('purchasePrice');
const pricePrefix = document.getElementById('pricePrefix');
const priceSuffix = document.getElementById('priceSuffix');
const priceHint = document.getElementById('priceHint');
const note = document.getElementById('note');
const noteCount = document.getElementById('noteCount');
document.querySelectorAll('input[name="priceMode"]').forEach(radio => {
radio.addEventListener('change', () => {
const isPct = radio.value === '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
? 'Percentage of the current market price you paid (e.g. 80 = 80%).'
: 'Enter the amount you paid.';
});
});
note.addEventListener('input', () => {
noteCount.textContent = `${note.value.length} / 255`;
});
document.getElementById('inventoryForm').addEventListener('submit', e => {
e.preventDefault();
const form = e.currentTarget;
if (!form.checkValidity()) { form.classList.add('was-validated'); return; }
form.classList.remove('was-validated');
// your save logic here — form data available via new FormData(form)
});
document.getElementById('inventoryForm').addEventListener('reset', () => {
document.getElementById('inventoryForm').classList.remove('was-validated');
noteCount.textContent = '0 / 255';
pricePrefix.classList.remove('d-none');
priceSuffix.classList.add('d-none');
priceInput.step = '0.01';
priceInput.max = '';
priceInput.placeholder = '0.00';
priceHint.textContent = 'Enter the amount you paid.';
document.getElementById('mode-dollar').checked = true;
});
})();
</script>
</div>
</div>
</div>