Merge branch 'feat/inventory'

This commit is contained in:
2026-05-26 05:32:26 -04:00
35 changed files with 3305 additions and 1073 deletions

View File

@@ -10,6 +10,10 @@ import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
import { Tooltip } from "bootstrap";
// auth check for inventory management features
//const { canAddInventory } = Astro.locals;
const canAddInventory = false;
export const partial = true;
export const prerender = false;
@@ -135,6 +139,17 @@ const conditionAttributes = (price: any) => {
}[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);
}
}
// ── Derive distinct variants available for this card ─────────────────────
const availableVariants = [...new Set(cardSkus.map(s => s.variant))].sort();
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`;
};
@@ -142,8 +157,8 @@ const ebaySearchUrl = (card: any) => {
const altSearchUrl = (card: any) => {
return `https://alt.xyz/browse?query=${encodeURIComponent(card?.productUrlName)}+${encodeURIComponent(card?.set?.setUrlName)}+${encodeURIComponent(card?.number)}&sortBy=newest_first`;
};
---
---
<div class="modal-dialog modal-dialog-centered modal-fullscreen-md-down modal-xl">
<div class="modal-content" data-card-id={card?.cardId}>
<div class="modal-header border-0">
@@ -187,8 +202,8 @@ const altSearchUrl = (card: any) => {
<span class="rarity-icon-large position-absolute bottom-0 end-0 d-inline"><RarityIcon rarity={card?.rarityName} /></span>
</div>
<div class="d-flex flex-column flex-lg-row justify-content-between mt-2">
<div class="text-secondary">{card?.set?.setCode}</div>
<div class="text-secondary">Illus<span class="d-none d-lg-inline">trator</span>: {card?.artist}</div>
<div class="text-secondary"><span class="d-flex d-xxl-none">{card?.set?.setCode}</span><span class="d-none d-xxl-flex">{card?.set?.setName}</span></div>
<div class="text-secondary">Illus<span class="d-none d-xxl-inline">trator</span>: {card?.artist}</div>
</div>
</div>
@@ -197,34 +212,36 @@ const altSearchUrl = (card: any) => {
<ul class="nav nav-tabs nav-fill border-0 me-3 mb-2" id="myTab" role="tablist">
<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">
<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>
</li>
<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">
<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>
</li>
<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">
<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>
</li>
<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">
<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>
</li>
<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">
<span class="d-none d-xxl-inline">Damaged</span><span class="d-xxl-none">DMG</span>
</button>
</li>
<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">
<span class="d-none d-xxl-inline">Inventory</span><span class="d-xxl-none">+/-</span>
<span class="d-none">Damaged</span><span class="d-inline">DMG</span>
</button>
</li>
{canAddInventory && (
<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">
<span class="d-none d-xxl-inline">Inventory</span><span class="d-xxl-none">+/-</span>
</button>
</li>
)}
</ul>
<div class="tab-content" id="myTabContent">
@@ -235,7 +252,7 @@ const altSearchUrl = (card: any) => {
<div class="d-flex flex-column gap-1">
<!-- Stat cards -->
<div class="d-flex flex-fill flex-row gap-1">
<div class="d-flex flex-fill flex-row gap-1 flex-wrap flex-lg-nowrap">
<div class="alert alert-dark rounded p-2 flex-fill d-flex flex-column mb-0">
<h6 class="mb-auto">Market Price</h6>
<p class="mb-0 mt-1">${price.marketPrice}</p>
@@ -307,12 +324,204 @@ const altSearchUrl = (card: any) => {
</div>
);
})}
{canAddInventory && (
<div class="tab-pane fade" id="nav-vendor" role="tabpanel" aria-labelledby="nav-vendor" tabindex="0">
<div class="row g-5">
<div class="col-12 col-md-6">
<h6 class="mt-1 mb-2">Add {card?.productName} to inventory</h6>
<div class="tab-pane fade" id="nav-vendor" role="tabpanel" aria-labelledby="nav-vendor" tabindex="0"></div>
<form id="inventoryForm" data-inventory-form novalidate>
<div class="row gx-3 gy-1">
<div class="col-12 col-lg-3">
<label for="quantity" class="form-label">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-12 col-lg-9">
<div class="d-flex justify-content-between align-items-start gap-2 flex-wrap">
<label for="purchasePrice" class="form-label">
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">Condition</label>
<div class="btn-group btn-group-sm condition-input w-100 col-12" 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>
<input type="hidden" name="variant" value={card?.variant} />
<div class="col-12">
<label for="catalogName" class="form-label">
Catalog
</label>
<input
type="text"
class="form-control"
id="catalogName"
name="catalogName"
list="catalogSuggestions"
placeholder="Default"
autocomplete="off"
maxlength="100"
/>
<datalist id="catalogSuggestions">
</datalist>
<div class="form-text">
Type a name or pick an existing catalog.
</div>
</div>
<div class="col-12">
<label for="note" class="form-label">
Note
</label>
<textarea
class="form-control"
id="note"
name="note"
rows="3"
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">
<h6 class="mt-1 mb-2">Inventory entries for {card?.productName}</h6>
<!-- 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" data-card-id={cardId} data-market-prices={JSON.stringify(marketPriceByCondition)}>
<span>Loading...</span>
</div>
</div>
</div>
</div>
)}
</div>
<!-- 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="alert alert-dark rounded p-2 mb-0">
<h6>Market Price History</h6>