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 });
|
||||
|
||||
@@ -6,9 +6,6 @@ import NavBar from '../components/NavBar.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
import pokedexList from '../data/pokedex.json';
|
||||
|
||||
const searchParams = Astro.url.searchParams;
|
||||
const query = searchParams.get('q') || '*';
|
||||
|
||||
// Get random # (0001–1025)
|
||||
const randomNumber = String(Math.floor(Math.random() * 1025) + 1).padStart(4, "0");
|
||||
|
||||
@@ -34,7 +31,7 @@ const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-12 col-md-5 offset-md-1">
|
||||
<div class="alert alert-warning border p-2" role="alert">
|
||||
<div id="reveal-hint" class="alert alert-warning border p-2" role="alert">
|
||||
<h4 class="alert-heading">Who's that Pokémon?</h4>
|
||||
<p class="mb-0">Click the image to reveal.</p>
|
||||
</div>
|
||||
@@ -47,12 +44,14 @@ const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
||||
<img class="w-100 starburst top-0 bottom-0 left-0 right-0" src="/404/glow.png" alt="" />
|
||||
|
||||
<img
|
||||
class="m-auto position-absolute w-75 top-0 left-25 bottom-10 right-0 d-block img-fluid masked-image top-50 start-50 translate-middle"
|
||||
class="m-auto position-absolute w-75 top-0 left-25 bottom-10 right-0 d-block img-fluid masked-image top-50 start-50 translate-middle pokemon-clickable"
|
||||
src={pokedexImage}
|
||||
alt={pokemonName}
|
||||
alt=""
|
||||
data-name={pokemonName}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
draggable="false"
|
||||
aria-label="Reveal the Pokémon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,21 +59,97 @@ const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
||||
|
||||
<!-- Pokémon name reveal -->
|
||||
<div class="col-12 text-center mt-3">
|
||||
<h3 id="pokemon-name" class="opacity-0 transition-opacity">???</h3>
|
||||
<h3
|
||||
id="pokemon-name"
|
||||
class="opacity-0 pokemon-transition"
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>???</h3>
|
||||
<button
|
||||
id="play-again"
|
||||
class="btn btn-primary mt-3 opacity-0 pokemon-transition"
|
||||
style="pointer-events: none;"
|
||||
aria-hidden="true"
|
||||
>
|
||||
Guess another Pokémon
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Footer slot="footer" />
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.pokemon-transition {
|
||||
transition: opacity 0.4s ease;
|
||||
}
|
||||
|
||||
.pokemon-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pokemon-clickable:focus-visible {
|
||||
outline: 3px solid #ffc107;
|
||||
outline-offset: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
@keyframes pokemon-pulse {
|
||||
0%, 100% { filter: brightness(0) drop-shadow(0 0 6px var(--bs-info-border-subtle)); }
|
||||
50% { filter: brightness(0) drop-shadow(0 0 18px var(--bs-info)); }
|
||||
}
|
||||
|
||||
.masked-image {
|
||||
filter: brightness(0);
|
||||
animation: pokemon-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
const img = document.querySelector('.masked-image') as HTMLImageElement | null;
|
||||
const nameEl = document.querySelector('#pokemon-name');
|
||||
const playAgainBtn = document.querySelector('#play-again') as HTMLButtonElement | null;
|
||||
const hintEl = document.querySelector('#reveal-hint');
|
||||
|
||||
function revealPokemon() {
|
||||
if (!img || !nameEl) return;
|
||||
|
||||
const doReveal = () => {
|
||||
img.classList.remove('masked-image');
|
||||
nameEl.textContent = img.dataset.name || "Unknown Pokémon";
|
||||
// Remove masked styles and interactivity from image
|
||||
img.classList.remove('masked-image', 'pokemon-clickable');
|
||||
img.removeAttribute('role');
|
||||
img.removeAttribute('tabindex');
|
||||
img.removeAttribute('aria-label');
|
||||
img.style.animation = '';
|
||||
|
||||
// Update alt text now that it's revealed
|
||||
img.alt = img.dataset.name || 'Unknown Pokémon';
|
||||
|
||||
// Reveal name
|
||||
nameEl.textContent = img.dataset.name || 'Unknown Pokémon';
|
||||
nameEl.classList.remove('opacity-0');
|
||||
dataLayer.push({ event: '404reveal', pokemonName: img.dataset.name });
|
||||
|
||||
// Update hint text
|
||||
if (hintEl) {
|
||||
hintEl.querySelector('p')!.textContent = "It's " + (img.dataset.name || 'Unknown Pokémon') + "!";
|
||||
}
|
||||
|
||||
// Show play again button
|
||||
if (playAgainBtn) {
|
||||
playAgainBtn.classList.remove('opacity-0');
|
||||
playAgainBtn.style.pointerEvents = '';
|
||||
playAgainBtn.removeAttribute('aria-hidden');
|
||||
}
|
||||
|
||||
// Fire analytics safely
|
||||
try {
|
||||
if (typeof dataLayer !== 'undefined') {
|
||||
dataLayer.push({ event: '404reveal', pokemonName: img.dataset.name });
|
||||
}
|
||||
} catch (e) {
|
||||
// Analytics unavailable, continue silently
|
||||
}
|
||||
};
|
||||
|
||||
if (!document.startViewTransition) {
|
||||
@@ -98,9 +173,8 @@ const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
||||
revealPokemon();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
|
||||
<Footer slot="footer" />
|
||||
</Layout>
|
||||
playAgainBtn?.addEventListener('click', () => {
|
||||
window.location.reload();
|
||||
});
|
||||
</script>
|
||||
@@ -242,45 +242,4 @@ const altSearchUrl = (card: any) => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
|
||||
async function copyImage(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);
|
||||
|
||||
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');
|
||||
} 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);
|
||||
}
|
||||
|
||||
</script>
|
||||
</div>
|
||||
Reference in New Issue
Block a user