refactored 404 page, fixed copy image toast on mobile and filtered missing images to exclude sealed
This commit is contained in:
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user