refactored 404 page, fixed copy image toast on mobile and filtered missing images to exclude sealed

This commit is contained in:
zach
2026-03-12 13:40:12 -04:00
parent c10e34cc34
commit 835a174da2
5 changed files with 242 additions and 178 deletions

View File

@@ -58,6 +58,63 @@ import BackToTop from "./BackToTop.astro"
<script is:inline>
(function () {
// ── Global helpers (called from card-modal partial onclick) ───────────────
window.copyImage = async function(img) {
try {
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
canvas.width = img.naturalWidth;
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');
});
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);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
showCopyToast('📋 Image URL copied!', '#198754');
}
}
} catch (err) {
console.error('Failed:', err);
showCopyToast('❌ Copy failed', '#dc3545');
}
};
function showCopyToast(message, color) {
const toast = document.createElement('div');
toast.textContent = message;
toast.style.cssText = `
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
background: ${color}; color: white; padding: 10px 20px;
border-radius: 8px; font-size: 14px; z-index: 9999;
opacity: 0; transition: opacity 0.2s ease;
pointer-events: none;
`;
document.body.appendChild(toast);
requestAnimationFrame(() => toast.style.opacity = '1');
setTimeout(() => {
toast.style.opacity = '0';
toast.addEventListener('transitionend', () => toast.remove());
}, 2000);
}
// ── State ────────────────────────────────────────────────────────────────
const cardIndex = [];
let currentCardId = null;
@@ -218,19 +275,25 @@ import BackToTop from "./BackToTop.astro"
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 = 'card-hero';
sourceImg.style.viewTransitionName = transitionName;
sourceImg.style.opacity = '0'; // hide original immediately after capture
}
const transition = document.startViewTransition(async () => {
// Clear source name BEFORE setting it on the destination
if (sourceImg) sourceImg.style.viewTransitionName = '';
target.innerHTML = html;
if (typeof htmx !== 'undefined') htmx.process(target);
const destImg = target.querySelector('img.card-image');
if (destImg) {
destImg.style.viewTransitionName = 'card-hero';
destImg.style.viewTransitionName = transitionName; // same unique name
if (!destImg.complete) {
await new Promise(resolve => {
destImg.addEventListener('load', resolve, { once: true });