Files
pokemon/src/pages/partials/card-modal.astro
2026-03-25 08:43:18 -04:00

645 lines
34 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
import ebay from "/vendors/ebay.svg?raw";
import SetIcon from '../../components/SetIcon.astro';
import EnergyIcon from '../../components/EnergyIcon.astro';
import RarityIcon from '../../components/RarityIcon.astro';
import { db } from '../../db/index';
import { priceHistory, skus } from '../../db/schema';
import { eq, inArray } from 'drizzle-orm';
import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
import { Tooltip } from "bootstrap";
export const partial = true;
export const prerender = false;
const searchParams = Astro.url.searchParams;
const cardId = Number(searchParams.get('cardId')) || 0;
const card = await db.query.cards.findFirst({
where: { cardId: Number(cardId) },
with: {
prices: true,
set: true,
}
});
function timeAgo(date: Date | null) {
if (!date) return "Not applicable";
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
const intervals: Record<string, number> = {
year: 31536000,
month: 2592000,
day: 86400,
hour: 3600,
minute: 60
};
for (const [unit, value] of Object.entries(intervals)) {
const count = Math.floor(seconds / value);
if (count >= 1) return `${count} ${unit}${count > 1 ? "s" : ""} ago`;
}
return "just now";
}
const calculatedAt = (() => {
if (!card?.prices?.length) return null;
const dates = card.prices
.map(p => p.calculatedAt)
.filter(d => d)
.map(d => new Date(d!));
if (!dates.length) return null;
return new Date(Math.max(...dates.map(d => d.getTime())));
})();
// ── Fetch price history + compute volatility ──────────────────────────────
const cardSkus = card?.prices?.length
? await db.select().from(skus).where(eq(skus.cardId, cardId))
: [];
const skuIds = cardSkus.map(s => s.skuId);
const historyRows = skuIds.length
? await db
.select({
skuId: priceHistory.skuId,
calculatedAt: priceHistory.calculatedAt,
marketPrice: priceHistory.marketPrice,
condition: skus.condition,
})
.from(priceHistory)
.innerJoin(skus, eq(priceHistory.skuId, skus.skuId))
.where(inArray(priceHistory.skuId, skuIds))
.orderBy(priceHistory.calculatedAt)
: [];
// Rolling 30-day cutoff for volatility calculation
const thirtyDaysAgo = new Date(Date.now() - 30 * 86_400_000);
const byCondition: Record<string, number[]> = {};
for (const row of historyRows) {
if (row.marketPrice == null) continue;
if (!row.calculatedAt) continue;
if (new Date(row.calculatedAt) < thirtyDaysAgo) continue;
const price = Number(row.marketPrice);
if (price <= 0) continue;
if (!byCondition[row.condition]) byCondition[row.condition] = [];
byCondition[row.condition].push(price);
}
function computeVolatility(prices: number[]): { label: string; monthlyVol: number } {
if (prices.length < 2) return { label: '—', monthlyVol: 0 };
const returns: number[] = [];
for (let i = 1; i < prices.length; i++) {
returns.push(Math.log(prices[i] / prices[i - 1]));
}
const mean = returns.reduce((a, b) => a + b, 0) / returns.length;
const variance = returns.reduce((sum, r) => sum + (r - mean) ** 2, 0) / (returns.length - 1);
const monthlyVol = Math.sqrt(variance) * Math.sqrt(30);
const label = monthlyVol >= 0.30 ? 'High'
: monthlyVol >= 0.15 ? 'Medium'
: 'Low';
return { label, monthlyVol: Math.round(monthlyVol * 100) / 100 };
}
const volatilityByCondition: Record<string, { label: string; monthlyVol: number }> = {};
for (const [condition, prices] of Object.entries(byCondition)) {
volatilityByCondition[condition] = computeVolatility(prices);
}
// ── Price history for chart (full history, not windowed) ──────────────────
const priceHistoryForChart = historyRows.map(row => ({
condition: row.condition,
calculatedAt: row.calculatedAt
? new Date(row.calculatedAt).toISOString().split('T')[0]
: null,
marketPrice: row.marketPrice,
})).filter(r => r.calculatedAt !== null);
// ── Determine which range buttons to show ────────────────────────────────
const now = Date.now();
const oldestDate = historyRows.length
? Math.min(...historyRows
.filter(r => r.calculatedAt)
.map(r => new Date(r.calculatedAt!).getTime()))
: now;
const dataSpanDays = (now - oldestDate) / 86_400_000;
const showRanges = {
'1m': dataSpanDays >= 1,
'3m': dataSpanDays >= 60,
'6m': dataSpanDays >= 180,
'1y': dataSpanDays >= 365,
'all': dataSpanDays >= 400,
};
const conditionOrder = ["Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged"];
const conditionAttributes = (price: any) => {
const condition: string = price?.condition || "Near Mint";
const vol = volatilityByCondition[condition] ?? { label: '—', monthlyVol: 0 };
const volatilityClass = (() => {
switch (vol.label) {
case "High": return "alert-danger";
case "Medium": return "alert-warning";
case "Low": return "alert-success";
default: return "alert-dark";
}
})();
const volatilityDisplay = vol.label === '—'
? '—'
: `${vol.label} (${(vol.monthlyVol * 100).toFixed(0)}%)`;
return {
"Near Mint": { label: "nav-nm", volatility: volatilityDisplay, volatilityClass, class: "show active" },
"Lightly Played": { label: "nav-lp", volatility: volatilityDisplay, volatilityClass },
"Moderately Played":{ label: "nav-mp", volatility: volatilityDisplay, volatilityClass },
"Heavily Played": { label: "nav-hp", volatility: volatilityDisplay, volatilityClass },
"Damaged": { label: "nav-dmg", volatility: volatilityDisplay, volatilityClass }
}[condition];
};
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`;
};
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">
<div class="container-fluid row align-items-center">
<div class="h4 card-title pe-2 col-sm-12 col-md-auto mb-sm-1">{card?.productName}</div>
<div class="text-secondary col-auto">{card?.number}</div>
<div class="text-light col-auto">{card?.variant}</div>
</div>
<div class="d-flex gap-2 align-items-center">
<button type="button" class="btn-close" aria-label="Close" data-bs-dismiss="modal"></button>
</div>
</div>
<div class="modal-body pt-0">
<div class="container-fluid">
<div class="card mb-2 border-0">
<div class="row g-4">
<!-- Card image column -->
<div class="col-sm-12 col-md-3">
<div class="position-relative mt-1">
<!-- card-image-wrap gives the modal image shimmer effects
without the hover lift/scale that image-grow has in main.scss -->
<div
class="card-image-wrap rounded-4"
data-energy={card?.energyType}
data-rarity={card?.rarityName}
data-variant={card?.variant}
data-name={card?.productName}
>
<img
src={`/static/cards/${card?.productId}.jpg`}
class="card-image w-100 img-fluid rounded-4"
alt={card?.productName}
crossorigin="anonymous"
onerror="this.onerror=null; this.src='/static/cards/default.jpg'; this.closest('.image-grow, .card-image-wrap')?.setAttribute('data-default','true')"
onclick="copyImage(this); dataLayer.push({'event': 'copiedImage'});"
/>
</div>
<span class="position-absolute top-50 start-0 d-inline"><FirstEditionIcon edition={card?.variant} /></span>
<span class="position-absolute bottom-0 start-0 d-inline"><SetIcon set={card?.set?.setCode} /></span>
<span class="position-absolute top-0 end-0 d-inline"><EnergyIcon energy={card?.energyType} /></span>
<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>
</div>
<!-- Tabs + price data column -->
<div class="col-sm-12 col-md-7">
<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">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">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">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">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">Damaged</span><span class="d-inline">DMG</span>
</button>
</li>
<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">
{card?.prices.slice().sort((a, b) => conditionOrder.indexOf(a.condition) - conditionOrder.indexOf(b.condition)).map((price) => {
const attributes = conditionAttributes(price);
return (
<div class={`tab-pane fade ${attributes?.label} ${attributes?.class ?? ''}`} id={`${attributes?.label}`} role="tabpanel" tabindex="0">
<div class="d-flex flex-column gap-1">
<!-- Stat cards -->
<div class="d-flex flex-fill flex-row gap-1">
<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>
</div>
<div class="alert alert-dark rounded p-2 flex-fill d-flex flex-column mb-0">
<h6 class="mb-auto">Lowest Price</h6>
<p class="mb-0 mt-1">${price.lowestPrice}</p>
</div>
<div class="alert alert-dark rounded p-2 flex-fill d-flex flex-column mb-0">
<h6 class="mb-auto">Highest Price</h6>
<p class="mb-0 mt-1">${price.highestPrice}</p>
</div>
<div class={`alert rounded p-2 flex-fill d-flex flex-column mb-0 ${attributes?.volatilityClass}`}>
<h6 class="mb-auto d-flex justify-content-between align-items-start">
<span class="me-1">Volatility</span>
<span
class="volatility-info float-end mt-0"
data-bs-toggle="tooltip"
data-bs-placement="top"
data-bs-container="body"
data-bs-custom-class="volatility-popover"
data-bs-trigger="hover focus click"
data-bs-html="true"
data-bs-title={`
<div class='tooltip-heading fw-bold mb-1'>Monthly Volatility</div>
<div class='small'>
<p class="mb-1">
<strong>What this measures:</strong> how much the market price tends to move day-to-day,
scaled up to a monthly expectation.
</p>
<p class="mb-0">
A card with <strong>30% volatility</strong> typically swings ±30% over a month.
</p>
</div>
`}
>
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" fill="currentColor" viewBox="0 0 16 16"> <path d="M8 15A7 7 0 1 1 8 1a7 7 0 0 1 0 14zm0 1A8 8 0 1 0 8 0a8 8 0 0 0 0 16z"/> <path d="m8.93 6.588-2.29.287-.082.38.45.083c.294.07.352.176.288.469l-.738 3.468c-.194.897.105 1.319.808 1.319.545 0 1.178-.252 1.465-.598l.088-.416c-.2.176-.492.246-.686.246-.275 0-.375-.193-.304-.533L8.93 6.588zM9 4.5a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/> </svg>
</span>
</h6>
<p class="mb-0 mt-1">{attributes?.volatility}</p>
</div>
</div>
<!-- Table only — chart is outside the tab panes -->
<div class="w-100">
<div class="alert alert-dark rounded p-2 mb-0 table-responsive">
<h6>Latest Verified Sales</h6>
<table class="table table-sm mb-0">
<caption class="small">Filtered to remove mismatched language variants</caption>
<thead class="table-dark">
<tr>
<th scope="col">Date</th>
<th scope="col">Title</th>
<th scope="col">Price</th>
</tr>
</thead>
<tbody>
<tr><td> </td><td> </td><td> </td></tr>
<tr><td> </td><td> </td><td> </td></tr>
<tr><td> </td><td> </td><td> </td></tr>
<tr><td> </td><td> </td><td> </td></tr>
<tr><td> </td><td> </td><td> </td></tr>
</tbody>
</table>
</div>
</div>
</div>
</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, theyll 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>
<!-- Chart lives permanently outside tab-content so Bootstrap never hides it. -->
<div class="d-block d-lg-flex gap-1 mt-1">
<div class="col-12">
<div class="alert alert-dark rounded p-2 mb-0">
<h6>Market Price History</h6>
<div id="priceHistoryEmpty" class="d-none text-secondary text-center py-4">
No sales data for the selected period/condition
</div>
<div class="position-relative" style="height: 200px;">
<canvas
id="priceHistoryChart"
class="price-history-chart"
data-card-id={card?.cardId}
data-history={JSON.stringify(priceHistoryForChart)}>
</canvas>
</div>
<div class="btn-group btn-group-sm d-flex flex-row gap-1 justify-content-end mt-2" role="group" aria-label="Time range">
{showRanges['1m'] && <button type="button" class="btn btn-dark price-range-btn active" data-range="1m">1M</button>}
{showRanges['3m'] && <button type="button" class="btn btn-dark price-range-btn" data-range="3m">3M</button>}
{showRanges['6m'] && <button type="button" class="btn btn-dark price-range-btn" data-range="6m">6M</button>}
{showRanges['1y'] && <button type="button" class="btn btn-dark price-range-btn" data-range="1y">1Y</button>}
{showRanges['all'] && <button type="button" class="btn btn-dark price-range-btn" data-range="all">All</button>}
</div>
</div>
</div>
</div>
</div>
<!-- External links column -->
<div class="col-sm-12 col-md-2 mt-0 mt-md-5 d-flex flex-row flex-md-column">
<a class="btn btn-dark mb-2 w-100 p-2" href={`https://www.tcgplayer.com/product/${card?.productId}`} target="_blank" onclick="dataLayer.push({'event': 'tcgplayerClick', 'tcgplayerUrl': this.getAttribute('href')});"><img src="/vendors/tcgplayer.webp"> <span class="d-none d-lg-inline">TCGPlayer</span></a>
<a class="btn btn-dark mb-2 w-100 p-2" href={`${ebaySearchUrl(card)}`} target="_blank" onclick="dataLayer.push({'event': 'ebayClick', 'ebayUrl': this.getAttribute('href')});"><span set:html={ebay} /></a>
<a class="btn btn-dark mb-2 w-100 p-2" href={`${altSearchUrl(card)}`} target="_blank" onclick="dataLayer.push({'event': 'altClick', 'altUrl': this.getAttribute('href')});"><svg width="48" height="20.16" viewBox="0 0 48 20" fill="none"><path d="M14.2761 19.9996H18.5308L11.6934 0.0712891H7.76953L14.2761 19.9996Z" fill="#ffffff"></path><path d="M6.17778 19.9986H6.14536L3.19643 11.2305L0 19.9988L6.17768 19.9989L6.17778 19.9986Z" fill="#ffffff"></path><path d="M24.7842 0H20.6759V19.9661H34.3427V16.5426H24.7842V0Z" fill="#ffffff"></path><path d="M41.6644 3.42355H47.4981V0H31.5033V3.42355H37.5561V19.9661H41.6644V3.42355Z" fill="#ffffff"></path></svg></a>
</div>
</div>
<div class="text-end my-0"><small class="text-body-tertiary">Prices last changed: {timeAgo(calculatedAt)}</small></div>
</div>
</div>
</div>
</div>
</div>