modified layout and made it so you can switch between card modals and keep the pricing chart
This commit is contained in:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user