reqrote volatility with proper standard deviation and added tooltip

This commit is contained in:
zach
2026-03-16 14:07:37 -04:00
parent a86dc08b50
commit 2f17912949
4 changed files with 93 additions and 28 deletions

View File

@@ -41,7 +41,7 @@
// @import 'bootstrap/scss/spinners'; // @import 'bootstrap/scss/spinners';
@import 'bootstrap/scss/tables'; @import 'bootstrap/scss/tables';
@import 'bootstrap/scss/toasts'; @import 'bootstrap/scss/toasts';
// @import 'bootstrap/scss/tooltip'; @import 'bootstrap/scss/tooltip';
@import 'bootstrap/scss/transitions'; @import 'bootstrap/scss/transitions';
// Optional helpers // Optional helpers

View File

@@ -380,6 +380,20 @@ $tiers: (
drop-shadow(0 4px 6px rgba(0, 0, 0, 0.2)); drop-shadow(0 4px 6px rgba(0, 0, 0, 0.2));
} }
.tooltip.volatility-popover .tooltip-inner {
background: #1d1f21;
color: #e9ecef;
padding: 0.9rem 1rem;
border-radius: 0.6rem;
text-align: left;
max-width: 260px;
border: 1px solid rgba(255, 255, 255, 0.08);
}
.tooltip.volatility-popover .tooltip-arrow::before {
border-top-color: #1d1f21 !important;
}
/* -------------------------------------------------- /* --------------------------------------------------
Pricing Pricing
-------------------------------------------------- */ -------------------------------------------------- */

View File

@@ -1,6 +1,6 @@
import * as bootstrap from 'bootstrap'; import * as bootstrap from 'bootstrap';
window.bootstrap = bootstrap; window.bootstrap = bootstrap;
import 'bootstrap/dist/js/bootstrap.bundle.min.js';
// trap browser back and close the modal if open // trap browser back and close the modal if open
const cardModal = document.getElementById('cardModal'); const cardModal = document.getElementById('cardModal');
@@ -25,3 +25,28 @@ cardModal.addEventListener('hide.bs.modal', () => {
history.back(); history.back();
} }
}); });
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
});
}
});
};
// Run on page load
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initTooltips);
} else {
initTooltips();
}
// 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

@@ -8,6 +8,8 @@ import { priceHistory, skus } from '../../db/schema';
import { eq, inArray } from 'drizzle-orm'; import { eq, inArray } from 'drizzle-orm';
import FirstEditionIcon from "../../components/FirstEditionIcon.astro"; import FirstEditionIcon from "../../components/FirstEditionIcon.astro";
import { Tooltip } from "bootstrap";
export const partial = true; export const partial = true;
export const prerender = false; export const prerender = false;
@@ -114,7 +116,6 @@ const priceHistoryForChart = historyRows.map(row => ({
})).filter(r => r.calculatedAt !== null); })).filter(r => r.calculatedAt !== null);
// ── Determine which range buttons to show ──────────────────────────────── // ── Determine which range buttons to show ────────────────────────────────
// Find the oldest data point to know what ranges are meaningful
const now = Date.now(); const now = Date.now();
const oldestDate = historyRows.length const oldestDate = historyRows.length
? Math.min(...historyRows ? Math.min(...historyRows
@@ -126,10 +127,10 @@ const dataSpanDays = (now - oldestDate) / 86_400_000;
const showRanges = { const showRanges = {
'1m': dataSpanDays >= 1, '1m': dataSpanDays >= 1,
'3m': dataSpanDays >= 60, // meaningful if at least 2 months of data '3m': dataSpanDays >= 60,
'6m': dataSpanDays >= 180, // meaningful if at least 6 months of data '6m': dataSpanDays >= 180,
'1y': dataSpanDays >= 365, // meaningful if at least 9 months of data '1y': dataSpanDays >= 365,
'all': dataSpanDays >= 400, // meaningful if more than ~13 months of data 'all': dataSpanDays >= 400,
}; };
const conditionOrder = ["Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged"]; const conditionOrder = ["Near Mint", "Lightly Played", "Moderately Played", "Heavily Played", "Damaged"];
@@ -250,24 +251,49 @@ const altSearchUrl = (card: any) => {
<div class="d-flex flex-column gap-1"> <div class="d-flex flex-column gap-1">
<!-- Stat cards --> <!-- Stat cards -->
<div class="d-flex flex-fill flex-row gap-1"> <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"> <div class="alert alert-dark rounded p-2 flex-fill d-flex flex-column mb-0">
<h6 class="mb-auto">Market Price</h6> <h6 class="mb-auto">Market Price</h6>
<p class="mb-0 mt-1">${price.marketPrice}</p> <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">Volatility</h6>
<p class="mb-0 mt-1">{attributes?.volatility}</p>
</div>
</div> </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>Volatility</span>
<span
class="volatility-info"
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 --> <!-- Table only — chart is outside the tab panes -->
<div class="w-100"> <div class="w-100">
@@ -320,9 +346,9 @@ const altSearchUrl = (card: any) => {
<div class="btn-group btn-group-sm d-flex flex-row gap-1 justify-content-end mt-2" role="group" aria-label="Time range"> <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['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['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 d-none" data-range="6m">6M</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 d-none" data-range="1y">1Y</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 d-none" data-range="all">All</button>} {showRanges['all'] && <button type="button" class="btn btn-dark price-range-btn" data-range="all">All</button>}
</div> </div>
</div> </div>
</div> </div>