modified layout and made it so you can switch between card modals and keep the pricing chart

This commit is contained in:
zach
2026-03-16 11:05:10 -04:00
parent 9c81a13c69
commit c4ebbfb060
4 changed files with 323 additions and 346 deletions

View File

@@ -41,7 +41,6 @@ import BackToTop from "./BackToTop.astro"
</div>
</div>
<!-- Modal nav buttons, rendered outside modal-content so they survive htmx swaps -->
<button id="modalPrevBtn" class="modal-nav-btn modal-nav-prev d-none" aria-label="Previous card">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z"/>
@@ -58,7 +57,7 @@ import BackToTop from "./BackToTop.astro"
<script is:inline>
(function () {
// ── Global helpers (called from card-modal partial onclick) ───────────────
// ── Global helpers ────────────────────────────────────────────────────────
window.copyImage = async function(img) {
try {
const canvas = document.createElement("canvas");
@@ -67,7 +66,6 @@ import BackToTop from "./BackToTop.astro"
canvas.height = img.naturalHeight;
ctx.drawImage(img, 0, 0);
// Try modern clipboard API first (requires HTTPS + permissions)
if (navigator.clipboard && navigator.clipboard.write) {
const blob = await new Promise((resolve, reject) => {
canvas.toBlob(b => b ? resolve(b) : reject(new Error('toBlob failed')), 'image/png');
@@ -75,13 +73,11 @@ import BackToTop from "./BackToTop.astro"
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
showCopyToast('📋 Image copied!', '#198754');
} else {
// Fallback: copy the image URL to clipboard as text
const url = img.src;
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(url);
showCopyToast('📋 Image URL copied!', '#198754');
} else {
// Last resort: execCommand (deprecated but broadly supported)
const input = document.createElement('input');
input.value = url;
document.body.appendChild(input);
@@ -115,7 +111,7 @@ import BackToTop from "./BackToTop.astro"
}, 2000);
}
// ── State ────────────────────────────────────────────────────────────────
// ── State ────────────────────────────────────────────────────────────────
const cardIndex = [];
let currentCardId = null;
let isNavigating = false;
@@ -177,6 +173,16 @@ import BackToTop from "./BackToTop.astro"
}
}
// ── Fire card-modal:swapped so the partial's script can init the chart ────
// Deferred one rAF so the canvas has real dimensions before Chart.js measures it.
function initChartAfterSwap(modal) {
const canvas = modal.querySelector('#priceHistoryChart');
if (!canvas) return;
requestAnimationFrame(() => {
modal.dispatchEvent(new CustomEvent('card-modal:swapped', { bubbles: false }));
});
}
async function loadCard(cardId, direction = null) {
if (!cardId || isNavigating) return;
isNavigating = true;
@@ -187,16 +193,18 @@ import BackToTop from "./BackToTop.astro"
const url = `/partials/card-modal?cardId=${cardId}`;
const { idx, total } = getAdjacentIds();
if (idx >= total - 3) {
tryTriggerSentinel();
}
if (idx >= total - 3) tryTriggerSentinel();
const doSwap = async () => {
const response = await fetch(url);
const html = await response.text();
if (modal._reconnectChartObserver) modal._reconnectChartObserver();
modal.innerHTML = html;
if (typeof htmx !== 'undefined') htmx.process(modal);
updateNavButtons(modal);
initChartAfterSwap(modal);
};
if (document.startViewTransition && direction) {
@@ -210,9 +218,7 @@ import BackToTop from "./BackToTop.astro"
isNavigating = false;
const { idx: newIdx, total: newTotal } = getAdjacentIds();
if (newIdx >= newTotal - 3) {
tryTriggerSentinel();
}
if (newIdx >= newTotal - 3) tryTriggerSentinel();
}
function navigatePrev() {
@@ -255,7 +261,7 @@ import BackToTop from "./BackToTop.astro"
else navigatePrev();
}, { passive: true });
// ── Hook into HTMX card-modal opens ──────────────────────────────────────
// ── HTMX card-modal opens ─────────────────────────────────────────────────
document.body.addEventListener('htmx:beforeRequest', async (e) => {
if (e.detail.elt.getAttribute('hx-target') !== '#cardModal') return;
@@ -270,30 +276,29 @@ import BackToTop from "./BackToTop.astro"
const target = document.getElementById('cardModal');
const sourceImg = cardEl?.querySelector('img');
// ── Fetch first, THEN transition ──────────────────────────────────────
const response = await fetch(url, { headers: { 'HX-Request': 'true' } });
if (!response.ok) throw new Error(`Failed to load modal: ${response.status}`);
const html = await response.text();
// Use a unique name per transition to avoid duplicate view-transition-name conflicts
const transitionName = `card-hero-${currentCardId}`;
try {
if (sourceImg) {
sourceImg.style.viewTransitionName = transitionName;
sourceImg.style.opacity = '0'; // hide original immediately after capture
sourceImg.style.opacity = '0';
}
const transition = document.startViewTransition(async () => {
// Clear source name BEFORE setting it on the destination
if (sourceImg) sourceImg.style.viewTransitionName = '';
if (target._reconnectChartObserver) target._reconnectChartObserver();
target.innerHTML = html;
if (typeof htmx !== 'undefined') htmx.process(target);
const destImg = target.querySelector('img.card-image');
if (destImg) {
destImg.style.viewTransitionName = transitionName; // same unique name
destImg.style.viewTransitionName = transitionName;
if (!destImg.complete) {
await new Promise(resolve => {
destImg.addEventListener('load', resolve, { once: true });
@@ -305,6 +310,7 @@ import BackToTop from "./BackToTop.astro"
await transition.finished;
updateNavButtons(target);
initChartAfterSwap(target);
} catch (err) {
console.error('[card-modal] transition failed:', err);
@@ -312,17 +318,18 @@ import BackToTop from "./BackToTop.astro"
} finally {
if (sourceImg) {
sourceImg.style.viewTransitionName = '';
sourceImg.style.opacity = ''; // restore after transition
sourceImg.style.opacity = '';
}
const destImg = target.querySelector('img.card-image');
if (destImg) destImg.style.viewTransitionName = '';
}
});
// ── Show/hide nav buttons with Bootstrap modal events ────────────────────
// ── Bootstrap modal events ────────────────────────────────────────────────
const cardModal = document.getElementById('cardModal');
cardModal.addEventListener('shown.bs.modal', () => {
updateNavButtons(cardModal);
initChartAfterSwap(cardModal);
});
cardModal.addEventListener('hidden.bs.modal', () => {
currentCardId = null;