sliding modals, view transitions, accessibility, etc, etc
This commit is contained in:
@@ -11,6 +11,9 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
server: {
|
||||||
|
allowedHosts: true,
|
||||||
|
},
|
||||||
adapter: node({ mode: "standalone", checkOrigin: false }),
|
adapter: node({ mode: "standalone", checkOrigin: false }),
|
||||||
output: "server",
|
output: "server",
|
||||||
security: {
|
security: {
|
||||||
|
|||||||
@@ -35,6 +35,31 @@ html {
|
|||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --------------------------------------------------
|
||||||
|
View Transitions
|
||||||
|
-------------------------------------------------- */
|
||||||
|
@view-transition {
|
||||||
|
navigation: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-group(card-image) {
|
||||||
|
animation-duration: 300ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
::view-transition-old(card-image),
|
||||||
|
::view-transition-new(card-image) {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: fade everything else */
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation-duration: 150ms;
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------
|
/* --------------------------------------------------
|
||||||
Layout
|
Layout
|
||||||
-------------------------------------------------- */
|
-------------------------------------------------- */
|
||||||
@@ -159,6 +184,33 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-nav-btn {
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
z-index: 1060; /* above modal backdrop (1050) */
|
||||||
|
background: rgba(255,255,255,0.1);
|
||||||
|
border: 1px solid rgba(255,255,255,0.2);
|
||||||
|
color: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
.modal-nav-btn:hover { background: rgba(255,255,255,0.25); }
|
||||||
|
.modal-nav-btn.d-none { display: none !important; }
|
||||||
|
.modal-nav-prev { left: 12px; }
|
||||||
|
.modal-nav-next { right: 12px; }
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.modal-nav-btn { display: none !important; } /* use swipe on mobile */
|
||||||
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------
|
/* --------------------------------------------------
|
||||||
Navigation Tabs & Tier Colors
|
Navigation Tabs & Tier Colors
|
||||||
-------------------------------------------------- */
|
-------------------------------------------------- */
|
||||||
@@ -218,7 +270,7 @@ $tiers: (
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* price-row alert left borders */
|
/* price-row alert left borders */
|
||||||
.nav-#{$name} div.alert-secondary {
|
.nav-#{$name} div.alert {
|
||||||
border-left: 3px solid $color;
|
border-left: 3px solid $color;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -521,15 +573,75 @@ $tiers: (
|
|||||||
/* --------------------------------------------------
|
/* --------------------------------------------------
|
||||||
Swipe Animation
|
Swipe Animation
|
||||||
-------------------------------------------------- */
|
-------------------------------------------------- */
|
||||||
@keyframes swipe-feedback {
|
|
||||||
0% { transform: scale(1); }
|
/* Smooth the hero image morph */
|
||||||
50% { transform: scale(0.95); }
|
::view-transition-group(card-hero) {
|
||||||
100% { transform: scale(1); }
|
animation-duration: 350ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade the old image out quickly so it doesn't ghost */
|
||||||
|
::view-transition-old(card-hero) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fade the new image in after it's in position */
|
||||||
|
::view-transition-new(card-hero) {
|
||||||
|
animation-duration: 350ms;
|
||||||
|
animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Suppress the default full-page crossfade so only the card morphs */
|
||||||
|
::view-transition-old(root),
|
||||||
|
::view-transition-new(root) {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sliding out (old content) */
|
||||||
|
::view-transition-old(.modal-content) {
|
||||||
|
animation: slide-out 200ms ease-in forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sliding in (new content) */
|
||||||
|
::view-transition-new(.modal-content) {
|
||||||
|
animation: slide-in 200ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Direction-aware — set via dataset.navDirection */
|
||||||
|
#cardModal[data-nav-direction="next"]::view-transition-old(.modal-content) {
|
||||||
|
animation: slide-out-left 200ms ease-in forwards;
|
||||||
|
}
|
||||||
|
#cardModal[data-nav-direction="next"]::view-transition-new(.modal-content) {
|
||||||
|
animation: slide-in-right 200ms ease-out forwards;
|
||||||
|
}
|
||||||
|
#cardModal[data-nav-direction="prev"]::view-transition-old(.modal-content) {
|
||||||
|
animation: slide-out-right 200ms ease-in forwards;
|
||||||
|
}
|
||||||
|
#cardModal[data-nav-direction="prev"]::view-transition-new(.modal-content) {
|
||||||
|
animation: slide-in-left 200ms ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The silhouette fades out while the colour image blooms in */
|
||||||
|
|
||||||
|
::view-transition-old(pokemon-reveal) {
|
||||||
|
animation: 300ms ease-in both fade-to-white;
|
||||||
|
}
|
||||||
|
::view-transition-new(pokemon-reveal) {
|
||||||
|
animation: 500ms ease-out both bloom-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-to-white {
|
||||||
|
to { opacity: 0; filter: brightness(3); }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bloom-in {
|
||||||
|
from { opacity: 0; filter: brightness(2) saturate(0); transform: scale(0.95); }
|
||||||
|
to { opacity: 1; filter: brightness(1) saturate(1); transform: scale(1); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------------------------------------
|
/* --------------------------------------------------
|
||||||
Input Fix (Safari)
|
Input Fix (Safari)
|
||||||
-------------------------------------------------- */
|
|
||||||
input[type="search"]::-webkit-search-cancel-button {
|
input[type="search"]::-webkit-search-cancel-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
@@ -539,3 +651,4 @@ input[type="search"]::-webkit-search-cancel-button {
|
|||||||
background-size: 1rem;
|
background-size: 1rem;
|
||||||
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAn0lEQVR42u3UMQrDMBBEUZ9WfQqDmm22EaTyjRMHAlM5K+Y7lb0wnUZPIKHlnutOa+25Z4D++MRBX98MD1V/trSppLKHqj9TTBWKcoUqffbUcbBBEhTjBOV4ja4l4OIAZThEOV6jHO8ARXD+gPPvKMABinGOrnu6gTNUawrcQKNCAQ7QeTxORzle3+sDfjJpPCqhJh7GixZq4rHcc9l5A9qZ+WeBhgEuAAAAAElFTkSuQmCC);
|
background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeCAYAAAA7MK6iAAAAn0lEQVR42u3UMQrDMBBEUZ9WfQqDmm22EaTyjRMHAlM5K+Y7lb0wnUZPIKHlnutOa+25Z4D++MRBX98MD1V/trSppLKHqj9TTBWKcoUqffbUcbBBEhTjBOV4ja4l4OIAZThEOV6jHO8ARXD+gPPvKMABinGOrnu6gTNUawrcQKNCAQ7QeTxORzle3+sDfjJpPCqhJh7GixZq4rHcc9l5A9qZ+WeBhgEuAAAAAElFTkSuQmCC);
|
||||||
}
|
}
|
||||||
|
-------------------------------------------------- */
|
||||||
@@ -1,35 +1,43 @@
|
|||||||
---
|
---
|
||||||
|
|
||||||
---
|
---
|
||||||
|
<button
|
||||||
<button type="button" class="btn btn-info p-2 rounded-circle" aria-label="Back to Top" id="btn-back-to-top" onclick="dataLayer.push({'event': 'backToTop'});">
|
type="button"
|
||||||
<span class="top-icon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M232 64C201.1 64 176 89.1 176 120L176 320.6C172.8 322.6 169.6 324.8 166.5 327.1L144 344C123.9 359.1 112 382.8 112 408L112 427.5C112 480.5 141.1 529.2 187.7 554.3L196 558.8C217 570.1 240.4 576 264.3 576L400 576C461.9 576 512 525.9 512 464L512 368C512 332.7 483.3 304 448 304C445.2 304 442.4 304.2 439.7 304.5C428.7 285.1 407.9 272 384 272C378.7 272 373.5 272.7 368.5 273.9C357.7 253.7 336.5 240 312 240C303.5 240 295.4 241.7 288 244.7L288 120C288 89.1 262.9 64 232 64zM208 120C208 106.7 218.7 96 232 96C245.3 96 256 106.7 256 120L256 277.5C256 284.6 260.6 290.8 267.4 292.8C274.2 294.8 281.4 292.2 285.4 286.3C291.2 277.6 301 272 312.1 272C327.7 272 340.7 283.2 343.5 297.9C344.5 303 347.9 307.4 352.7 309.5C357.5 311.6 363 311.3 367.5 308.6C372.3 305.7 378 304 384.1 304C398.8 304 411.3 314 415 327.6C416.2 332 419.2 335.7 423.3 337.7C427.4 339.7 432.1 339.9 436.4 338.3C440 336.9 444 336.1 448.2 336.1C465.9 336.1 480.2 350.4 480.2 368.1L480.2 464.1C480.2 508.3 444.4 544.1 400.2 544.1L264.5 544.1C246 544.1 227.7 539.5 211.4 530.7L211.4 530.7L203.1 526.2C166.6 506.6 144 468.7 144 427.5L144 408C144 392.9 151.1 378.7 163.2 369.6L176 360L176 408C176 416.8 183.2 424 192 424C200.8 424 208 416.8 208 408L208 120z"/></svg></span>
|
class="btn btn-info p-2 rounded-circle"
|
||||||
|
aria-label="Back to Top"
|
||||||
|
aria-hidden="true"
|
||||||
|
id="btn-back-to-top"
|
||||||
|
style="display:none"
|
||||||
|
>
|
||||||
|
<span class="top-icon">
|
||||||
|
<svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
|
||||||
|
<path d="M232 64C201.1 64 176 89.1 176 120L176 320.6C172.8 322.6 169.6 324.8 166.5 327.1L144 344C123.9 359.1 112 382.8 112 408L112 427.5C112 480.5 141.1 529.2 187.7 554.3L196 558.8C217 570.1 240.4 576 264.3 576L400 576C461.9 576 512 525.9 512 464L512 368C512 332.7 483.3 304 448 304C445.2 304 442.4 304.2 439.7 304.5C428.7 285.1 407.9 272 384 272C378.7 272 373.5 272.7 368.5 273.9C357.7 253.7 336.5 240 312 240C303.5 240 295.4 241.7 288 244.7L288 120C288 89.1 262.9 64 232 64zM208 120C208 106.7 218.7 96 232 96C245.3 96 256 106.7 256 120L256 277.5C256 284.6 260.6 290.8 267.4 292.8C274.2 294.8 281.4 292.2 285.4 286.3C291.2 277.6 301 272 312.1 272C327.7 272 340.7 283.2 343.5 297.9C344.5 303 347.9 307.4 352.7 309.5C357.5 311.6 363 311.3 367.5 308.6C372.3 305.7 378 304 384.1 304C398.8 304 411.3 314 415 327.6C416.2 332 419.2 335.7 423.3 337.7C427.4 339.7 432.1 339.9 436.4 338.3C440 336.9 444 336.1 448.2 336.1C465.9 336.1 480.2 350.4 480.2 368.1L480.2 464.1C480.2 508.3 444.4 544.1 400.2 544.1L264.5 544.1C246 544.1 227.7 539.5 211.4 530.7L211.4 530.7L203.1 526.2C166.6 506.6 144 468.7 144 427.5L144 408C144 392.9 151.1 378.7 163.2 369.6L176 360L176 408C176 416.8 183.2 424 192 424C200.8 424 208 416.8 208 408L208 120z"/>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
//Get the button
|
const mybutton = document.getElementById("btn-back-to-top");
|
||||||
let mybutton = document.getElementById("btn-back-to-top");
|
|
||||||
|
|
||||||
// When the user scrolls down 20px from the top of the document, show the button
|
function setButtonVisibility(visible: boolean) {
|
||||||
window.onscroll = function () {
|
if (!mybutton) return;
|
||||||
scrollFunction();
|
mybutton.style.display = visible ? "block" : "none";
|
||||||
};
|
mybutton.setAttribute("aria-hidden", visible ? "false" : "true");
|
||||||
|
|
||||||
function scrollFunction() {
|
|
||||||
if (
|
|
||||||
document.body.scrollTop > 20 ||
|
|
||||||
document.documentElement.scrollTop > 20
|
|
||||||
) {
|
|
||||||
mybutton.style.display = "block";
|
|
||||||
} else {
|
|
||||||
mybutton.style.display = "none";
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// When the user clicks on the button, scroll to the top of the document
|
|
||||||
mybutton.addEventListener("click", backToTop);
|
|
||||||
|
|
||||||
function backToTop() {
|
function scrollFunction() {
|
||||||
document.body.scrollTop = 0;
|
const scrolled = document.body.scrollTop > 20 || document.documentElement.scrollTop > 20;
|
||||||
document.documentElement.scrollTop = 0;
|
setButtonVisibility(scrolled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function backToTop() {
|
||||||
|
dataLayer.push({ event: "backToTop" });
|
||||||
|
window.scrollTo({ top: 0, behavior: "smooth" });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mybutton) {
|
||||||
|
mybutton.addEventListener("click", backToTop);
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener("scroll", scrollFunction);
|
||||||
</script>
|
</script>
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
import BackToTop from "./BackToTop.astro"
|
import BackToTop from "./BackToTop.astro"
|
||||||
---
|
---
|
||||||
<div class="row mb-4">
|
<div class="row mb-4">
|
||||||
<div class="col-md-2 display-sm-none">
|
<div class="col-md-2">
|
||||||
<div class="h5 d-none">Inventory management placeholder</div>
|
<div class="h5 d-none">Inventory management placeholder</div>
|
||||||
<div class="offcanvas offcanvas-start" data-bs-backdrop="static" tabindex="-1" id="filterBar" aria-labelledby="filterBarLabel">
|
<div class="offcanvas offcanvas-start" data-bs-backdrop="static" tabindex="-1" id="filterBar" aria-labelledby="filterBarLabel">
|
||||||
<div class="offcanvas-header">
|
<div class="offcanvas-header">
|
||||||
@@ -15,16 +15,256 @@ import BackToTop from "./BackToTop.astro"
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12 col-md-10 mt-0">
|
<div class="col-sm-12 col-md-10 mt-0">
|
||||||
<div id="activeFilters" class="mb-2 d-flex align-items-center justify-content-end small"></div>
|
<div class="d-flex flex-row align-items-center mb-2">
|
||||||
<div id="cardGrid" class="row g-xxl-3 g-2 row-cols-2 row-cols-md-3 row-cols-xl-4 row-cols-xxxl-5"></div>
|
<div id="sortBy" class="mb-2 d-flex align-items-center justify-content-start small d-none">
|
||||||
<div id="notfound"></div>
|
<button class="btn btn-sm btn-dark dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">Sort by</button>
|
||||||
|
<ul class="dropdown-menu dropdown-menu-dark">
|
||||||
|
<li><a class="dropdown-item" href="#">Price: High to Low</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Price: Low to High</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Set: Newest to Oldest</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Set: Oldest to Newest</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Card Number: Ascending</a></li>
|
||||||
|
<li><a class="dropdown-item" href="#">Card Number: Descending</a></li>
|
||||||
|
</ul>
|
||||||
|
<div id="totalResults"></div>
|
||||||
|
</div>
|
||||||
|
<div id="activeFilters"></div>
|
||||||
|
</div>
|
||||||
|
<div id="cardGrid" aria-live="polite" class="row g-xxl-3 g-2 row-cols-2 row-cols-md-3 row-cols-xl-4 row-cols-xxxl-5"></div>
|
||||||
|
<div id="notfound" aria-live="polite"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal fade card-modal" id="cardModal" tabindex="-1" aria-labelledby="cardModalLabel" aria-hidden="true" transition:name="">
|
|
||||||
|
<div class="modal card-modal" id="cardModal" tabindex="-1" aria-labelledby="cardModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog modal-dialog-centered modal-fullscreen-md-down modal-xl">
|
<div class="modal-dialog modal-dialog-centered modal-fullscreen-md-down modal-xl">
|
||||||
<div class="modal-content">
|
<div class="modal-content p-2">Loading...</div>
|
||||||
Loading...
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<BackToTop>
|
|
||||||
|
<!-- 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"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button id="modalNextBtn" class="modal-nav-btn modal-nav-next d-none" aria-label="Next 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="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<BackToTop />
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
|
||||||
|
// ── State ────────────────────────────────────────────────────────────────
|
||||||
|
const cardIndex = [];
|
||||||
|
let currentCardId = null;
|
||||||
|
let isNavigating = false;
|
||||||
|
|
||||||
|
// ── Register cards as HTMX loads them ────────────────────────────────────
|
||||||
|
const cardGrid = document.getElementById('cardGrid');
|
||||||
|
const gridObserver = new MutationObserver((mutations) => {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
for (const node of mutation.addedNodes) {
|
||||||
|
if (node.nodeType !== 1) continue;
|
||||||
|
const triggers = node.querySelectorAll
|
||||||
|
? node.querySelectorAll('[data-card-id]')
|
||||||
|
: [];
|
||||||
|
for (const el of triggers) {
|
||||||
|
const id = Number(el.getAttribute('data-card-id'));
|
||||||
|
if (id && !cardIndex.includes(id)) cardIndex.push(id);
|
||||||
|
}
|
||||||
|
if (node.dataset?.cardId) {
|
||||||
|
const id = Number(node.dataset.cardId);
|
||||||
|
if (id && !cardIndex.includes(id)) cardIndex.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
gridObserver.observe(cardGrid, { childList: true, subtree: true });
|
||||||
|
|
||||||
|
// ── Navigation helpers ────────────────────────────────────────────────────
|
||||||
|
function getAdjacentIds() {
|
||||||
|
const idx = cardIndex.indexOf(currentCardId);
|
||||||
|
return {
|
||||||
|
prev: idx > 0 ? cardIndex[idx - 1] : null,
|
||||||
|
next: idx < cardIndex.length - 1 ? cardIndex[idx + 1] : null,
|
||||||
|
idx,
|
||||||
|
total: cardIndex.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateNavButtons(modal) {
|
||||||
|
const prevBtn = document.getElementById('modalPrevBtn');
|
||||||
|
const nextBtn = document.getElementById('modalNextBtn');
|
||||||
|
if (!modal || !modal.classList.contains('show')) {
|
||||||
|
prevBtn.classList.add('d-none');
|
||||||
|
nextBtn.classList.add('d-none');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { prev, next } = getAdjacentIds();
|
||||||
|
prevBtn.classList.toggle('d-none', prev === null);
|
||||||
|
nextBtn.classList.toggle('d-none', next === null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Trigger infinite scroll sentinel ─────────────────────────────────────
|
||||||
|
function tryTriggerSentinel() {
|
||||||
|
const sentinel = cardGrid.querySelector('[hx-trigger="revealed"]');
|
||||||
|
if (!sentinel) return;
|
||||||
|
if (typeof htmx !== 'undefined') {
|
||||||
|
htmx.trigger(sentinel, 'revealed');
|
||||||
|
} else {
|
||||||
|
sentinel.scrollIntoView({ behavior: 'instant', block: 'end' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCard(cardId, direction = null) {
|
||||||
|
if (!cardId || isNavigating) return;
|
||||||
|
isNavigating = true;
|
||||||
|
|
||||||
|
currentCardId = cardId;
|
||||||
|
|
||||||
|
const modal = document.getElementById('cardModal');
|
||||||
|
const url = `/partials/card-modal?cardId=${cardId}`;
|
||||||
|
|
||||||
|
const { idx, total } = getAdjacentIds();
|
||||||
|
if (idx >= total - 3) {
|
||||||
|
tryTriggerSentinel();
|
||||||
|
}
|
||||||
|
|
||||||
|
const doSwap = async () => {
|
||||||
|
const response = await fetch(url);
|
||||||
|
const html = await response.text();
|
||||||
|
modal.innerHTML = html;
|
||||||
|
if (typeof htmx !== 'undefined') htmx.process(modal);
|
||||||
|
updateNavButtons(modal);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (document.startViewTransition && direction) {
|
||||||
|
modal.dataset.navDirection = direction;
|
||||||
|
await document.startViewTransition(doSwap).finished;
|
||||||
|
delete modal.dataset.navDirection;
|
||||||
|
} else {
|
||||||
|
await doSwap();
|
||||||
|
}
|
||||||
|
|
||||||
|
isNavigating = false;
|
||||||
|
|
||||||
|
const { idx: newIdx, total: newTotal } = getAdjacentIds();
|
||||||
|
if (newIdx >= newTotal - 3) {
|
||||||
|
tryTriggerSentinel();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigatePrev() {
|
||||||
|
const { prev } = getAdjacentIds();
|
||||||
|
if (prev) loadCard(prev, 'prev');
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateNext() {
|
||||||
|
const { next } = getAdjacentIds();
|
||||||
|
if (next) loadCard(next, 'next');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Nav button clicks ─────────────────────────────────────────────────────
|
||||||
|
document.getElementById('modalPrevBtn').addEventListener('click', navigatePrev);
|
||||||
|
document.getElementById('modalNextBtn').addEventListener('click', navigateNext);
|
||||||
|
|
||||||
|
// ── Keyboard ──────────────────────────────────────────────────────────────
|
||||||
|
document.addEventListener('keydown', (e) => {
|
||||||
|
const modal = document.getElementById('cardModal');
|
||||||
|
if (!modal.classList.contains('show')) return;
|
||||||
|
if (e.key === 'ArrowLeft') { e.preventDefault(); navigatePrev(); }
|
||||||
|
if (e.key === 'ArrowRight') { e.preventDefault(); navigateNext(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Touch / swipe ─────────────────────────────────────────────────────────
|
||||||
|
let touchStartX = 0;
|
||||||
|
let touchStartY = 0;
|
||||||
|
const SWIPE_THRESHOLD = 50;
|
||||||
|
|
||||||
|
document.getElementById('cardModal').addEventListener('touchstart', (e) => {
|
||||||
|
touchStartX = e.touches[0].clientX;
|
||||||
|
touchStartY = e.touches[0].clientY;
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
document.getElementById('cardModal').addEventListener('touchend', (e) => {
|
||||||
|
const dx = e.changedTouches[0].clientX - touchStartX;
|
||||||
|
const dy = e.changedTouches[0].clientY - touchStartY;
|
||||||
|
if (Math.abs(dx) < SWIPE_THRESHOLD || Math.abs(dy) > Math.abs(dx)) return;
|
||||||
|
if (dx < 0) navigateNext();
|
||||||
|
else navigatePrev();
|
||||||
|
}, { passive: true });
|
||||||
|
|
||||||
|
// ── Hook into HTMX card-modal opens ──────────────────────────────────────
|
||||||
|
document.body.addEventListener('htmx:beforeRequest', async (e) => {
|
||||||
|
if (e.detail.elt.getAttribute('hx-target') !== '#cardModal') return;
|
||||||
|
|
||||||
|
const cardEl = e.detail.elt.closest('[data-card-id]');
|
||||||
|
if (cardEl) currentCardId = Number(cardEl.getAttribute('data-card-id'));
|
||||||
|
|
||||||
|
if (!document.startViewTransition) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const url = e.detail.requestConfig.path;
|
||||||
|
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();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (sourceImg) {
|
||||||
|
sourceImg.style.viewTransitionName = 'card-hero';
|
||||||
|
sourceImg.style.opacity = '0'; // hide original immediately after capture
|
||||||
|
}
|
||||||
|
|
||||||
|
const transition = document.startViewTransition(async () => {
|
||||||
|
target.innerHTML = html;
|
||||||
|
if (typeof htmx !== 'undefined') htmx.process(target);
|
||||||
|
|
||||||
|
const destImg = target.querySelector('img.card-image');
|
||||||
|
if (destImg) {
|
||||||
|
destImg.style.viewTransitionName = 'card-hero';
|
||||||
|
if (!destImg.complete) {
|
||||||
|
await new Promise(resolve => {
|
||||||
|
destImg.addEventListener('load', resolve, { once: true });
|
||||||
|
destImg.addEventListener('error', resolve, { once: true });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await transition.finished;
|
||||||
|
updateNavButtons(target);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[card-modal] transition failed:', err);
|
||||||
|
e.detail.elt.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||||
|
} finally {
|
||||||
|
if (sourceImg) {
|
||||||
|
sourceImg.style.viewTransitionName = '';
|
||||||
|
sourceImg.style.opacity = ''; // restore after transition
|
||||||
|
}
|
||||||
|
const destImg = target.querySelector('img.card-image');
|
||||||
|
if (destImg) destImg.style.viewTransitionName = '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Show/hide nav buttons with Bootstrap modal events ────────────────────
|
||||||
|
const cardModal = document.getElementById('cardModal');
|
||||||
|
cardModal.addEventListener('shown.bs.modal', () => {
|
||||||
|
updateNavButtons(cardModal);
|
||||||
|
});
|
||||||
|
cardModal.addEventListener('hidden.bs.modal', () => {
|
||||||
|
currentCardId = null;
|
||||||
|
updateNavButtons(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -21,13 +21,16 @@ const energyMap = {
|
|||||||
"Fire": fire,
|
"Fire": fire,
|
||||||
"Water": water,
|
"Water": water,
|
||||||
"Steel": steel,
|
"Steel": steel,
|
||||||
|
"Metal": steel,
|
||||||
"Colorless": colorless,
|
"Colorless": colorless,
|
||||||
"Fighting": fighting,
|
"Fighting": fighting,
|
||||||
"Psychic": psychic,
|
"Psychic": psychic,
|
||||||
"Electric": electric,
|
"Electric": electric,
|
||||||
|
"Lightning": electric,
|
||||||
};
|
};
|
||||||
|
|
||||||
const svg = energyMap[energy as keyof typeof energyMap] ?? "";
|
const svg = energyMap[energy as keyof typeof energyMap] ?? "";
|
||||||
|
if (!svg && energy) console.warn(`No energy icon found for: ${energy}`);
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="energy-icon shadow-filter" set:html={svg}></div>
|
<div class="energy-icon shadow-filter" role="img" aria-label={energy} set:html={svg}></div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ const { edition } = Astro.props;
|
|||||||
|
|
||||||
const editionMap = {
|
const editionMap = {
|
||||||
"1st Edition Holofoil": first,
|
"1st Edition Holofoil": first,
|
||||||
|
"1st Edition": first,
|
||||||
};
|
};
|
||||||
|
|
||||||
const svg = editionMap[edition as keyof typeof editionMap] ?? "";
|
const svg = editionMap[edition as keyof typeof editionMap] ?? "";
|
||||||
|
|||||||
@@ -1,15 +1,17 @@
|
|||||||
---
|
---
|
||||||
import EnergyWheel from './EnergyWheel.astro';
|
|
||||||
import '/src/assets/css/main.scss';
|
|
||||||
---
|
|
||||||
<footer class="bd-footer py-4 py-md-5 mt-0 bottom-0 bg-body-tertiary">
|
|
||||||
<div class="container py-4 py-md-5 px-4 px-md-3 text-body-secondary">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-3 mb-3">
|
|
||||||
|
|
||||||
</div>
|
---
|
||||||
<div class="col mb-3 align-items-end">
|
<footer class="bd-footer py-4 py-md-5 mt-0 bg-body-tertiary">
|
||||||
<a class="btn btn-outline-success rounded p-2 float-end" href="/contact">Contact Us <svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path opacity=".25" d="M112 176L404 176C411.9 206.7 431 233 456.6 250.2L320 353.9L112 196.1L112 176zM112 256.3L305.5 403.1L320 414.1L334.5 403.1L509.2 270.6C515.3 271.5 521.6 272 528 272L528 464L112 464L112 256.3z"/><path d="M528 64C572.2 64 608 99.8 608 144C608 188.2 572.2 224 528 224C483.8 224 448 188.2 448 144C448 99.8 483.8 64 528 64zM88 128L401 128C400.3 133.2 400 138.6 400 144C400 155 401.4 165.8 404 176L112 176L112 196.1L320 353.9L456.6 250.3C472.1 260.7 489.9 267.8 509.2 270.7L334.5 403.2L320 414.2L305.5 403.2L112 256.4L112 464.1L528 464.1L528 272.1C545 272.1 561.2 268.8 576 262.8L576 512.1L64 512.1L64 128.1L88 128.1z"/></svg></a>
|
<div class="container py-4 py-md-5 px-4 px-md-3 text-body-secondary">
|
||||||
|
<div class="row justify-content-end">
|
||||||
|
<div class="col mb-3">
|
||||||
|
<a class="btn btn-outline-success rounded p-2 float-end" href="/contact">
|
||||||
|
Contact Us
|
||||||
|
<svg aria-hidden="true" class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
|
||||||
|
<path opacity=".25" d="M112 176L404 176C411.9 206.7 431 233 456.6 250.2L320 353.9L112 196.1L112 176zM112 256.3L305.5 403.1L320 414.1L334.5 403.1L509.2 270.6C515.3 271.5 521.6 272 528 272L528 464L112 464L112 256.3z"/>
|
||||||
|
<path d="M528 64C572.2 64 608 99.8 608 144C608 188.2 572.2 224 528 224C483.8 224 448 188.2 448 144C448 99.8 483.8 64 528 64zM88 128L401 128C400.3 133.2 400 138.6 400 144C400 155 401.4 165.8 404 176L112 176L112 196.1L320 353.9L456.6 250.3C472.1 260.7 489.9 267.8 509.2 270.7L334.5 403.2L320 414.2L305.5 403.2L112 256.4L112 464.1L528 464.1L528 272.1C545 272.1 561.2 268.8 576 262.8L576 512.1L64 512.1L64 128.1L88 128.1z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
---
|
---
|
||||||
import '/src/assets/css/main.scss';
|
|
||||||
export const prerender = false;
|
|
||||||
---
|
---
|
||||||
<nav class="navbar navbar-expand sticky-top bg-dark" data-bs-theme="dark">
|
<nav class="navbar navbar-expand sticky-top bg-dark" data-bs-theme="dark" aria-label="Main navigation">
|
||||||
<div class="container container-fluid">
|
<div class="container">
|
||||||
<a class="navbar-brand d-flex" href="/">
|
<a class="navbar-brand d-flex" href="/">
|
||||||
<span class="h3 d-none d-md-flex">Rigid's App Thing</span><span class="h3 d-md-none d-flex m-auto">RAT</span>
|
<span class="h3 d-none d-md-flex">Rigid's App Thing</span><span aria-hidden="true" class="h3 d-md-none d-flex m-auto">RAT</span>
|
||||||
</a>
|
</a>
|
||||||
<slot name="navItems"/>
|
<slot name="navItems"/>
|
||||||
<slot name="searchInput"/>
|
<slot name="searchInput"/>
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
---
|
---
|
||||||
import '/src/assets/css/main.scss';
|
|
||||||
---
|
---
|
||||||
<div class="navbar-collapse" id="navbarNav">
|
<div class="navbar-collapse" id="navbarNav" aria-labelledby="navbarToggler">
|
||||||
<ul class="navbar-nav ms-auto">
|
<ul class="navbar-nav ms-auto">
|
||||||
<li class="nav-item d-flex">
|
<li class="nav-item d-flex">
|
||||||
<a class="nav-link btn btn-warning rounded p-2" href="/pokemon"><span class="d-inline-block d-md-none">Cards</span> <svg class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path opacity=".4" d="M256 519.9L256 576L576 576L576 128L378.8 128C408.7 239.7 438.6 351.3 468.5 463C397.7 482 326.8 501 256 519.9z"/><path d="M43.5 113L352.6 30.2L468.6 462.9L159.5 545.7z"/></svg></a>
|
<a class="nav-link btn btn-warning rounded p-2" href="/pokemon" aria-label="Cards">
|
||||||
|
<span class="d-inline-block d-md-none" aria-hidden="true">Cards</span>
|
||||||
|
<svg aria-hidden="true" class="nav-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640">
|
||||||
|
<path opacity=".4" d="M256 519.9L256 576L576 576L576 128L378.8 128C408.7 239.7 438.6 351.3 468.5 463C397.7 482 326.8 501 256 519.9z"/>
|
||||||
|
<path d="M43.5 113L352.6 30.2L468.6 462.9L159.5 545.7z"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
import '/src/assets/css/main.scss';
|
|
||||||
---
|
|
||||||
|
|
||||||
|
---
|
||||||
<header class="header-top w-100">
|
<header class="header-top w-100">
|
||||||
<div class="header-wrap">
|
<div class="header-wrap">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ const rarityMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const svg = rarityMap[rarity as keyof typeof rarityMap] ?? "";
|
const svg = rarityMap[rarity as keyof typeof rarityMap] ?? "";
|
||||||
|
if (!svg && rarity) console.warn(`No rarity icon found for: ${rarity}`);
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="rarity-icon shadow-filter" set:html={svg}></div>
|
<div class="rarity-icon shadow-filter" role="img" aria-label={rarity} set:html={svg}></div>
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import { Show } from '@clerk/astro/components'
|
|||||||
|
|
||||||
<Show when="signed-in">
|
<Show when="signed-in">
|
||||||
<form class="d-flex ms-2" role="search" id="searchform" hx-post="/partials/cards" hx-target="#cardGrid" hx-trigger="load, submit" hx-vals='{"start":"0"}' hx-on--after-request="afterUpdate()" hx-on--before-request="beforeSearch()">
|
<form class="d-flex ms-2" role="search" id="searchform" hx-post="/partials/cards" hx-target="#cardGrid" hx-trigger="load, submit" hx-vals='{"start":"0"}' hx-on--after-request="afterUpdate()" hx-on--before-request="beforeSearch()">
|
||||||
<a class="btn btn-secondary btn-lg me-2" data-bs-toggle="offcanvas" href="#filterBar" role="button" aria-controls="filterBar"><span class="d-block d-md-none filter-icon mt-1"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path opacity=".4" d="M96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160L96 160z"/><path d="M66.4 147.8C71.4 135.8 83.1 128 96 128L544 128C556.9 128 568.6 135.8 573.6 147.8C578.6 159.8 575.8 173.5 566.7 182.7L384 365.3L384 544C384 556.9 376.2 568.6 364.2 573.6C352.2 578.6 338.5 575.8 329.3 566.7L265.3 502.7C259.3 496.7 255.9 488.6 255.9 480.1L256 365.3L73.4 182.6C64.2 173.5 61.5 159.7 66.4 147.8zM544 160L96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160z"/></svg></span><span class="d-none d-md-block">Filters</span></a>
|
<a class="btn btn-secondary btn-lg me-2" data-bs-toggle="offcanvas" href="#filterBar" role="button" aria-controls="filterBar" aria-label="filter"><span class="d-block d-md-none filter-icon mt-1"><svg aria-hidden="true" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path opacity=".4" d="M96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160L96 160z"/><path d="M66.4 147.8C71.4 135.8 83.1 128 96 128L544 128C556.9 128 568.6 135.8 573.6 147.8C578.6 159.8 575.8 173.5 566.7 182.7L384 365.3L384 544C384 556.9 376.2 568.6 364.2 573.6C352.2 578.6 338.5 575.8 329.3 566.7L265.3 502.7C259.3 496.7 255.9 488.6 255.9 480.1L256 365.3L73.4 182.6C64.2 173.5 61.5 159.7 66.4 147.8zM544 160L96 160L283.3 347.3C286.3 350.3 288 354.4 288 358.6L288 480L352 544L352 358.6C352 354.4 353.7 350.3 356.7 347.3L544 160z"/></svg></span><span class="d-none d-md-block">Filters</span></a>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input type="hidden" name="start" id="start" value="0" />
|
<input type="hidden" name="start" id="start" value="0" />
|
||||||
<input type="search" name="q" class="form-control form-control-lg" placeholder="Search cards..." />
|
<input type="search" name="q" class="form-control form-control-lg" placeholder="Search cards..." />
|
||||||
<button type="submit" class="btn btn-danger btn-lg border border-danger-subtle border-start-0" value="" onclick="const q = document.querySelector('[name=q]').value; dataLayer.push({ event: 'view_search_results', search_term: q });">
|
<button type="submit" class="btn btn-danger btn-lg border border-danger-subtle border-start-0" aria-label="search" value="" onclick="const q = this.closest('form').querySelector('[name=q]').value; dataLayer.push({ event: 'view_search_results', search_term: q });">
|
||||||
<svg class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M432 272C432 183.6 360.4 112 272 112C183.6 112 112 183.6 112 272C112 360.4 183.6 432 272 432C360.4 432 432 360.4 432 272zM401.1 435.1C365.7 463.2 320.8 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272C480 320.8 463.2 365.7 435.1 401.1L569 535C578.4 544.4 578.4 558.1 569 567.5C559.6 575.9 544.4 575.9 536 567.5L401.1 435.1z"/></svg>
|
<svg aria-hidden="true" class="search-button d-block" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640"><path d="M432 272C432 183.6 360.4 112 272 112C183.6 112 112 183.6 112 272C112 360.4 183.6 432 272 432C360.4 432 432 360.4 432 272zM401.1 435.1C365.7 463.2 320.8 480 272 480C157.1 480 64 386.9 64 272C64 157.1 157.1 64 272 64C386.9 64 480 157.1 480 272C480 320.8 463.2 365.7 435.1 401.1L569 535C578.4 544.4 578.4 558.1 569 567.5C559.6 575.9 544.4 575.9 536 567.5L401.1 435.1z"/></svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ import mega_evolutions from "/src/svg/set/mega_evolutions.svg?raw";
|
|||||||
import phantasmal_flames from "/src/svg/set/phantasmal_flames.svg?raw";
|
import phantasmal_flames from "/src/svg/set/phantasmal_flames.svg?raw";
|
||||||
import destined_rivals from "/src/svg/set/destined_rivals.svg?raw";
|
import destined_rivals from "/src/svg/set/destined_rivals.svg?raw";
|
||||||
import surging_sparks from "/src/svg/set/surging_sparks.svg?raw";
|
import surging_sparks from "/src/svg/set/surging_sparks.svg?raw";
|
||||||
|
import team_rocket from "/src/svg/set/team_rocket.svg?raw";
|
||||||
|
|
||||||
const { set } = Astro.props;
|
const { set } = Astro.props;
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ const setMap = {
|
|||||||
"JU": jungle,
|
"JU": jungle,
|
||||||
"FO": fossil,
|
"FO": fossil,
|
||||||
"B2": base_set_2,
|
"B2": base_set_2,
|
||||||
"TR": battle_styles,
|
"TR": team_rocket,
|
||||||
"G1": gym_heroes,
|
"G1": gym_heroes,
|
||||||
"G2": gym_challenge,
|
"G2": gym_challenge,
|
||||||
"SI": southern_islands,
|
"SI": southern_islands,
|
||||||
@@ -254,6 +255,7 @@ const setMap = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const svg = setMap[set as keyof typeof setMap] ?? "";
|
const svg = setMap[set as keyof typeof setMap] ?? "";
|
||||||
|
if (!svg && set) console.warn(`No set icon found for: ${set}`);
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="set-icon shadow-filter" set:html={svg}></div>
|
<div class="set-icon shadow-filter" role="img" aria-label={set} set:html={svg}></div>
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
---
|
---
|
||||||
import '/src/assets/css/main.scss';
|
import '/src/assets/css/main.scss';
|
||||||
|
const { title } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en" data-bs-theme="dark">
|
<html lang="en" data-bs-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<script is:inline>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
</script>
|
|
||||||
<!-- Google Tag Manager -->
|
<!-- Google Tag Manager -->
|
||||||
<script is:inline>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
<script is:inline>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
|
||||||
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
|
||||||
@@ -17,10 +15,10 @@ import '/src/assets/css/main.scss';
|
|||||||
<!-- End Google Tag Manager -->
|
<!-- End Google Tag Manager -->
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="htmx-config" content='{"historyCacheSize": 50}'></meta>
|
<meta name="htmx-config" content='{"historyCacheSize": 50}'/>
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<title>Rigid's App Thing</title>
|
<title>{title}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Google Tag Manager (noscript) -->
|
<!-- Google Tag Manager (noscript) -->
|
||||||
@@ -40,7 +38,7 @@ import '/src/assets/css/main.scss';
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.8/dist/htmx.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js"></script>
|
||||||
<script src="../assets/js/main.js"></script>
|
<script src="../assets/js/main.js"></script>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
|
export const prerender = false;
|
||||||
import Layout from '../layouts/Main.astro';
|
import Layout from '../layouts/Main.astro';
|
||||||
import NavItems from '../components/NavItems.astro';
|
import NavItems from '../components/NavItems.astro';
|
||||||
import NavBar from '../components/NavBar.astro';
|
import NavBar from '../components/NavBar.astro';
|
||||||
export const prerender = false;
|
|
||||||
import pokedexList from '../data/pokedex.json';
|
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
|
import pokedexList from '../data/pokedex.json';
|
||||||
|
|
||||||
const searchParams = Astro.url.searchParams;
|
const searchParams = Astro.url.searchParams;
|
||||||
const query = searchParams.get('q') || '*';
|
const query = searchParams.get('q') || '*';
|
||||||
@@ -21,13 +21,13 @@ const pokemon = pokedexList.find(p => p["#"] === randomNumber);
|
|||||||
// If not found (rare), fallback
|
// If not found (rare), fallback
|
||||||
const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
||||||
---
|
---
|
||||||
<Layout>
|
<Layout title="404 - Page Not Found">
|
||||||
<NavBar slot="navbar">
|
<NavBar slot="navbar">
|
||||||
<NavItems slot="navItems" />
|
<NavItems slot="navItems" />
|
||||||
</NavBar>
|
</NavBar>
|
||||||
<div class="row mb-4" slot="page">
|
<div class="row mb-4" slot="page">
|
||||||
<div class="col-12 col-md-6">
|
<div class="col-12 col-md-6">
|
||||||
<h1 class="mb-4">404 - Page Not Found</h1>
|
<h1 class="mb-4">404<br/>Page Not Found</h1>
|
||||||
<h4>Sorry, the page you are looking for does not exist.</h4>
|
<h4>Sorry, the page you are looking for does not exist.</h4>
|
||||||
<p class="copy-big my-4">
|
<p class="copy-big my-4">
|
||||||
Return to the <a href="/">home page</a> or search for another <a href="/pokemon">Pokémon</a>.
|
Return to the <a href="/">home page</a> or search for another <a href="/pokemon">Pokémon</a>.
|
||||||
@@ -40,14 +40,20 @@ const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-0 ratio ratio-1x1 position-relative overflow-hidden d-flex justify-items-center">
|
<div class="p-0 ratio ratio-1x1 position-relative overflow-hidden d-flex justify-items-center">
|
||||||
<img class="whos-that-pokemon position-absolute h-100" src="/404/lines.gif">
|
<img class="whos-that-pokemon position-absolute h-100" src="/404/lines.gif" alt="" />
|
||||||
|
|
||||||
<div class="d-flex flex-col-reverse flex-lg-row">
|
<div class="d-flex flex-column-reverse flex-lg-row">
|
||||||
<div class="">
|
<div>
|
||||||
<img class="w-100 starburst top-0 bottom-0 left-0 right-0" src="/404/glow.png">
|
<img class="w-100 starburst top-0 bottom-0 left-0 right-0" src="/404/glow.png" alt="" />
|
||||||
|
|
||||||
<!-- ✨ Name is placed in a data attribute for later use -->
|
<img
|
||||||
<img class="m-auto position-absolute w-50 top-0 left-25 bottom-10 right-0 d-block img-fluid masked-image top-50 start-50 translate-middle" src={pokedexImage} alt={pokemonName} data-name={pokemonName} onclick="dataLayer.push({'event': '404reveal','pokemonName': this.getAttribute('data-name')});"/>
|
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"
|
||||||
|
src={pokedexImage}
|
||||||
|
alt={pokemonName}
|
||||||
|
data-name={pokemonName}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,16 +63,44 @@ const pokemonName = pokemon?.Name || "Unknown Pokémon";
|
|||||||
<h3 id="pokemon-name" class="opacity-0 transition-opacity">???</h3>
|
<h3 id="pokemon-name" class="opacity-0 transition-opacity">???</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
const img = document.querySelector('.masked-image');
|
const img = document.querySelector('.masked-image') as HTMLImageElement | null;
|
||||||
const nameEl = document.querySelector('#pokemon-name');
|
const nameEl = document.querySelector('#pokemon-name');
|
||||||
|
|
||||||
img?.addEventListener('click', () => {
|
function revealPokemon() {
|
||||||
|
if (!img || !nameEl) return;
|
||||||
|
|
||||||
|
const doReveal = () => {
|
||||||
img.classList.remove('masked-image');
|
img.classList.remove('masked-image');
|
||||||
nameEl.textContent = img.dataset.name || "Unknown Pokémon";
|
nameEl.textContent = img.dataset.name || "Unknown Pokémon";
|
||||||
nameEl.classList.remove('opacity-0');
|
nameEl.classList.remove('opacity-0');
|
||||||
|
dataLayer.push({ event: '404reveal', pokemonName: img.dataset.name });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!document.startViewTransition) {
|
||||||
|
doReveal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.style.viewTransitionName = 'pokemon-reveal';
|
||||||
|
|
||||||
|
document.startViewTransition(() => {
|
||||||
|
doReveal();
|
||||||
|
}).finished.then(() => {
|
||||||
|
img.style.viewTransitionName = '';
|
||||||
});
|
});
|
||||||
</script>
|
}
|
||||||
|
|
||||||
|
img?.addEventListener('click', revealPokemon);
|
||||||
|
img?.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
revealPokemon();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Footer slot="footer" />
|
<Footer slot="footer" />
|
||||||
</Layout>
|
</Layout>
|
||||||
@@ -1,28 +1,37 @@
|
|||||||
---
|
---
|
||||||
|
export const prerender = false;
|
||||||
import Layout from '../layouts/Main.astro';
|
import Layout from '../layouts/Main.astro';
|
||||||
import NavItems from '../components/NavItems.astro';
|
import NavItems from '../components/NavItems.astro';
|
||||||
import NavBar from '../components/NavBar.astro';
|
import NavBar from '../components/NavBar.astro';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
export const prerender = false;
|
|
||||||
---
|
---
|
||||||
<Layout>
|
<Layout title="Contact Us">
|
||||||
<NavBar slot="navbar">
|
<NavBar slot="navbar">
|
||||||
<NavItems slot="navItems" />
|
<NavItems slot="navItems" />
|
||||||
</NavBar>
|
</NavBar>
|
||||||
<div class="row mb-4" slot="page">
|
<div class="row mb-4" slot="page">
|
||||||
|
<div class="col-12">
|
||||||
<h1>Contact Us</h1>
|
<h1>Contact Us</h1>
|
||||||
|
</div>
|
||||||
<div class="col-12 col-md-8 col-lg-6">
|
<div class="col-12 col-md-8 col-lg-6">
|
||||||
<form action="https://docs.google.com/forms/u/0/d/e/1FAIpQLSc4F7VZjZ6ImWnNRqzMLyAWnyGQdEC3Nr2xtbzugewky239kg/formResponse" method="POST" id="contactForm">
|
<form action="https://docs.google.com/forms/u/0/d/e/1FAIpQLSc4F7VZjZ6ImWnNRqzMLyAWnyGQdEC3Nr2xtbzugewky239kg/formResponse" method="POST" id="contactForm" target="hidden-iframe">
|
||||||
|
|
||||||
|
<!-- Honeypot field to deter spam -->
|
||||||
|
<div style="display:none" aria-hidden="true">
|
||||||
|
<label for="honeypot">Leave this field blank</label>
|
||||||
|
<input type="text" id="honeypot" name="honeypot" tabindex="-1" autocomplete="off" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Name input -->
|
<!-- Name input -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="name" class="form-label">Full Name</label>
|
<label for="name" class="form-label">Full Name</label>
|
||||||
<input type="text" class="form-control" id="name" name="entry.563494744" required>
|
<input type="text" class="form-control" id="name" name="entry.563494744" required />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Email address input -->
|
<!-- Email address input -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="email" class="form-label">Email address</label>
|
<label for="email" class="form-label">Email address</label>
|
||||||
<input type="email" class="form-control" id="email" name="entry.577942868" aria-describedby="emailHelp" required>
|
<input type="email" class="form-control" id="email" name="entry.577942868" aria-describedby="emailHelp" required />
|
||||||
<div id="emailHelp" class="form-text">We'll never share your email with anyone else.</div>
|
<div id="emailHelp" class="form-text">We'll never share your email with anyone else.</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -33,10 +42,51 @@ export const prerender = false;
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Submit button -->
|
<!-- Submit button -->
|
||||||
<button type="submit" class="btn btn-light">Submit</button>
|
<button type="submit" class="btn btn-light" id="submitBtn">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
<!-- Hidden iframe absorbs the Google Forms redirect -->
|
||||||
|
<iframe name="hidden-iframe" style="display:none" aria-hidden="true"></iframe>
|
||||||
|
|
||||||
|
<!-- Success message (hidden until submission) -->
|
||||||
|
<div id="successMsg" class="alert alert-success mt-3 d-none" role="alert">
|
||||||
|
Thanks for reaching out! We'll get back to you soon.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Footer slot="footer" />
|
<Footer slot="footer" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const form = document.getElementById('contactForm') as HTMLFormElement | null;
|
||||||
|
const submitBtn = document.getElementById('submitBtn') as HTMLButtonElement | null;
|
||||||
|
const successMsg = document.getElementById('successMsg');
|
||||||
|
const honeypot = document.getElementById('honeypot') as HTMLInputElement | null;
|
||||||
|
const iframe = document.querySelector('iframe[name="hidden-iframe"]') as HTMLIFrameElement | null;
|
||||||
|
|
||||||
|
form?.addEventListener('submit', (e) => {
|
||||||
|
// Honeypot check — bail silently if filled in by a bot
|
||||||
|
if (honeypot?.value) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!submitBtn || !successMsg) return;
|
||||||
|
|
||||||
|
submitBtn.disabled = true;
|
||||||
|
submitBtn.textContent = 'Sending...';
|
||||||
|
});
|
||||||
|
|
||||||
|
// iframe load fires after Google Forms redirects into it — treat as success
|
||||||
|
iframe?.addEventListener('load', () => {
|
||||||
|
if (!form || !submitBtn || !successMsg) return;
|
||||||
|
|
||||||
|
// Ignore the initial empty load before any submission
|
||||||
|
if (!submitBtn.disabled) return;
|
||||||
|
|
||||||
|
form.reset();
|
||||||
|
form.classList.add('d-none');
|
||||||
|
successMsg.classList.remove('d-none');
|
||||||
|
dataLayer.push({ event: 'contact_form_submit' });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,22 +1,24 @@
|
|||||||
---
|
---
|
||||||
|
export const prerender = false;
|
||||||
import Layout from '../layouts/Main.astro';
|
import Layout from '../layouts/Main.astro';
|
||||||
import NavItems from '../components/NavItems.astro';
|
import NavItems from '../components/NavItems.astro';
|
||||||
import NavBar from '../components/NavBar.astro';
|
import NavBar from '../components/NavBar.astro';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
export const prerender = false;
|
|
||||||
import { Show, SignInButton, SignUpButton, SignOutButton } from '@clerk/astro/components'
|
import { Show, SignInButton, SignUpButton, SignOutButton } from '@clerk/astro/components'
|
||||||
---
|
---
|
||||||
<Layout>
|
<Layout title="Rigid's App Thing">
|
||||||
<NavBar slot="navbar">
|
<NavBar slot="navbar">
|
||||||
<NavItems slot="navItems" />
|
<NavItems slot="navItems" />
|
||||||
</NavBar>
|
</NavBar>
|
||||||
<div class="row mb-4" slot="page">
|
<div class="row mb-4" slot="page">
|
||||||
|
<div class="col-12">
|
||||||
<h1>Rigid's App Thing</h1>
|
<h1>Rigid's App Thing</h1>
|
||||||
<h5 class="text-secondary">(working title)</h5>
|
<p class="text-secondary">(working title)</p>
|
||||||
|
</div>
|
||||||
<div class="col-12 col-md-6 mb-2">
|
<div class="col-12 col-md-6 mb-2">
|
||||||
<h4 class="mt-3">Welcome!</h4>
|
<h2 class="mt-3">Welcome!</h2>
|
||||||
<p class="mt-2">
|
<p class="mt-2">
|
||||||
You've been selected to participate in the closed beta! This single page web application is meant to elevate condition/variant data for the Pokemon TCG. In future iterations, we will add vendor inventory/collection management features, as well.</p>
|
You've been selected to participate in the closed beta! This single page web application is meant to elevate condition/variant data for the Pokemon TCG. In future iterations, we will add vendor inventory/collection management features, as well.
|
||||||
</p>
|
</p>
|
||||||
<p class="my-2">
|
<p class="my-2">
|
||||||
After the closed beta is complete, the app will move into a more open beta. Feel free to play "Who's that Pokémon?" with the random Pokémon generator <a href="/404">here</a>. Refresh the page to see a new Pokémon!
|
After the closed beta is complete, the app will move into a more open beta. Feel free to play "Who's that Pokémon?" with the random Pokémon generator <a href="/404">here</a>. Refresh the page to see a new Pokémon!
|
||||||
@@ -25,28 +27,19 @@ import { Show, SignInButton, SignUpButton, SignOutButton } from '@clerk/astro/co
|
|||||||
<a href="/pokemon" class="btn btn-warning mt-2">Take me to the cards</a>
|
<a href="/pokemon" class="btn btn-warning mt-2">Take me to the cards</a>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-6 d-flex flex-row gap-5 justify-content-end">
|
<div class="col-12 col-md-6 d-flex justify-content-end align-items-start mt-3">
|
||||||
<div>
|
<div class="d-flex gap-3">
|
||||||
<Show when="signed-out">
|
<Show when="signed-out">
|
||||||
<!-- Using Bootstrap btn classes -->
|
|
||||||
<SignInButton asChild mode="modal">
|
<SignInButton asChild mode="modal">
|
||||||
<button class="btn btn-success">
|
<button class="btn btn-success">Sign In</button>
|
||||||
Sign In
|
|
||||||
</button>
|
|
||||||
</SignInButton>
|
</SignInButton>
|
||||||
<SignUpButton asChild mode="modal">
|
<SignUpButton asChild mode="modal">
|
||||||
<button class="btn btn-dark">
|
<button class="btn btn-dark">Request Access</button>
|
||||||
Request Access
|
|
||||||
</button>
|
|
||||||
</SignUpButton>
|
</SignUpButton>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Show when="signed-in">
|
<Show when="signed-in">
|
||||||
<SignOutButton asChild mode="modal">
|
<SignOutButton asChild>
|
||||||
<button class="btn btn-danger">
|
<button class="btn btn-danger">Sign Out</button>
|
||||||
Sign Out
|
|
||||||
</button>
|
|
||||||
</SignOutButton>
|
</SignOutButton>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -73,31 +73,27 @@ const conditionOrder = ["Near Mint", "Lightly Played", "Moderately Played", "Hea
|
|||||||
|
|
||||||
const conditionAttributes = (price: any) => {
|
const conditionAttributes = (price: any) => {
|
||||||
const volatility = (() => {
|
const volatility = (() => {
|
||||||
const current = price?.marketPrice;
|
const market = price?.marketPrice;
|
||||||
const low = price?.lowestPrice;
|
const low = price?.lowestPrice;
|
||||||
const high = price?.highestPrice;
|
const high = price?.highestPrice;
|
||||||
const median = price?.medianPrice;
|
|
||||||
|
|
||||||
if (current === null || low === null || high === null) return "Indeterminate";
|
if (market == null || low == null || high == null || Number(market) === 0) {
|
||||||
|
return "Indeterminate";
|
||||||
|
}
|
||||||
|
|
||||||
const range = Number(high) - Number(low);
|
const spreadPct = (Number(high) - Number(low)) / Number(market) * 100;
|
||||||
if (range <= 0) return "Low";
|
|
||||||
|
|
||||||
const position = (Number(current) - Number(low)) / range;
|
if (spreadPct >= 81) return "High";
|
||||||
if (position > 0.76) return "High";
|
if (spreadPct >= 59) return "Medium";
|
||||||
if (position > 0.49) return "Medium";
|
|
||||||
return "Low";
|
return "Low";
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Updated logic:
|
|
||||||
// ❗ If High / Medium / Low → never "alert-secondary"
|
|
||||||
// ❗ If Indeterminate → "alert-secondary"
|
|
||||||
const volatilityClass = (() => {
|
const volatilityClass = (() => {
|
||||||
switch (volatility) {
|
switch (volatility) {
|
||||||
case "High": return "alert-danger";
|
case "High": return "alert-danger";
|
||||||
case "Medium": return "alert-warning";
|
case "Medium": return "alert-warning";
|
||||||
case "Low": return "alert-success";
|
case "Low": return "alert-success";
|
||||||
default: return "alert-secondary"; // Only for Indeterminate
|
default: return "alert-dark"; // Indeterminate
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
@@ -115,6 +111,10 @@ const conditionAttributes = (price: any) => {
|
|||||||
const ebaySearchUrl = (card: any) => {
|
const ebaySearchUrl = (card: any) => {
|
||||||
return `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(card?.productUrlName)}+${encodeURIComponent(card?.set?.setUrlName)}+${encodeURIComponent(card?.number)}&LH_Sold=1&Graded=No&_dcat=183454`;
|
return `https://www.ebay.com/sch/i.html?_nkw=${encodeURIComponent(card?.productUrlName)}+${encodeURIComponent(card?.set?.setUrlName)}+${encodeURIComponent(card?.number)}&LH_Sold=1&Graded=No&_dcat=183454`;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const altSearchUrl = (card: any) => {
|
||||||
|
return `https://alt.xyz/browse?query=${encodeURIComponent(card?.productUrlName)}+${encodeURIComponent(card?.set?.setUrlName)}+${encodeURIComponent(card?.number)}&sortBy=newest_first`;
|
||||||
|
};
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="modal-dialog modal-dialog-centered modal-fullscreen-md-down modal-xl">
|
<div class="modal-dialog modal-dialog-centered modal-fullscreen-md-down modal-xl">
|
||||||
@@ -126,16 +126,6 @@ const ebaySearchUrl = (card: any) => {
|
|||||||
<div class="text-light col-auto">{card?.variant}</div>
|
<div class="text-light col-auto">{card?.variant}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex gap-2 align-items-center">
|
<div class="d-flex gap-2 align-items-center">
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary card-nav-prev d-none" title="Previous card (← or swipe right)" aria-label="Previous card">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="15 18 9 12 15 6"></polyline>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-secondary card-nav-next d-none" title="Next card (→ or swipe left)" aria-label="Next card">
|
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
||||||
<polyline points="9 18 15 12 9 6"></polyline>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn-close" aria-label="Close" data-bs-dismiss="modal"></button>
|
<button type="button" class="btn-close" aria-label="Close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -178,29 +168,56 @@ const ebaySearchUrl = (card: any) => {
|
|||||||
<div class={`tab-pane fade ${attributes?.label} ${attributes?.class}`} id={`${attributes?.label}`} role="tabpanel" tabindex="0">
|
<div class={`tab-pane fade ${attributes?.label} ${attributes?.class}`} id={`${attributes?.label}`} role="tabpanel" tabindex="0">
|
||||||
<div class="d-block gap-1 d-lg-flex">
|
<div class="d-block gap-1 d-lg-flex">
|
||||||
<div class="d-flex flex-row flex-lg-column gap-1 col-12 col-lg-2 mb-1">
|
<div class="d-flex flex-row flex-lg-column gap-1 col-12 col-lg-2 mb-1">
|
||||||
<div class="alert alert-secondary rounded p-2 flex-fill mb-1">
|
<div class="alert alert-dark rounded p-2 flex-fill d-flex flex-column mb-1">
|
||||||
<h6>Market Price</h6>
|
<h6 class="mb-auto">Market Price</h6>
|
||||||
<p class="pb-0">${price.marketPrice}</p>
|
<p class="mb-0 mt-1">${price.marketPrice}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-secondary rounded p-2 flex-fill mb-1">
|
<div class="alert alert-dark rounded p-2 flex-fill d-flex flex-column mb-1">
|
||||||
<h6>Lowest Price</h6>
|
<h6 class="mb-auto">Lowest Price</h6>
|
||||||
<p class="pb-0">${price.lowestPrice}</p>
|
<p class="mb-0 mt-1">${price.lowestPrice}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-secondary rounded p-2 flex-fill mb-1">
|
<div class="alert alert-dark rounded p-2 flex-fill d-flex flex-column mb-1">
|
||||||
<h6>Highest Price</h6>
|
<h6 class="mb-auto">Highest Price</h6>
|
||||||
<p class="pb-0">${price.highestPrice}</p>
|
<p class="mb-0 mt-1">${price.highestPrice}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class={`alert alert-secondary rounded p-2 flex-fill mb-1 ${attributes?.volatilityClass}`}>
|
<div class={`alert alert-secondary rounded p-2 flex-fill d-flex flex-column mb-1 ${attributes?.volatilityClass}`}>
|
||||||
<h6>Volatility</h6>
|
<h6 class="mb-auto">Volatility</h6>
|
||||||
<p class="pb-0 small">{attributes?.volatility}</p>
|
<p class="mb-0 mt-1">{attributes?.volatility}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-flex flex-column gap-1 col-12 col-lg-10 mb-0 me-2 clearfix">
|
<div class="d-flex flex-column gap-1 col-12 col-lg-10 mb-0 me-2 clearfix">
|
||||||
<div class="alert alert-secondary rounded p-2 mb-1">
|
<div class="alert alert-dark rounded p-2 mb-1 table-responsive">
|
||||||
<h6>Latest Sales</h6>
|
<h6>Latest Verified Sales</h6>
|
||||||
|
<table class="table table-sm mb-0">
|
||||||
|
<caption class="small">Filtered to remove mismatched language variants</caption>
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Date</th>
|
||||||
|
<th scope="col">Title</th>
|
||||||
|
<th scope="col">Price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="alert alert-dark rounded p-2 mb-1">
|
||||||
|
<h6>Market Price History</h6>
|
||||||
|
<div class="position-relative" style="height: 200px;">
|
||||||
|
<canvas id={`priceChart-${price.priceId}`} class="price-history-chart" data-card-id={card?.cardId} data-condition={price.condition}></canvas>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm d-flex flex-row gap-1 justify-content-end" role="group" aria-label="Time range">
|
||||||
|
<button type="button" class="btn btn-dark price-range-btn active" data-range="1m">1M</button>
|
||||||
|
<button type="button" class="btn btn-dark price-range-btn" data-range="3m">3M</button>
|
||||||
|
<button type="button" class="btn btn-dark price-range-btn" data-range="6m">6M</button>
|
||||||
|
<button type="button" class="btn btn-dark price-range-btn" data-range="1y">1Y</button>
|
||||||
|
<button type="button" class="btn btn-dark price-range-btn" data-range="all">All</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="alert alert-secondary rounded p-2 mb-1">
|
|
||||||
<h6>Placeholder for graph</h6>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -214,9 +231,10 @@ const ebaySearchUrl = (card: any) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-12 col-md-2 mt-0 mt-md-5">
|
<div class="col-sm-12 col-md-2 mt-0 mt-md-5 d-flex flex-row flex-md-column">
|
||||||
<a class="btn btn-outline-light mb-2 w-100" href={`https://www.tcgplayer.com/product/${card?.productId}`} target="_blank" onclick="dataLayer.push({'event': 'tcgplayerClick', 'tcgplayerUrl': this.getAttribute('href')});"><img src="/vendors/tcgplayer.webp"> <span class="d-none d-lg-inline">TCGPlayer</span></a>
|
<a class="btn btn-dark mb-2 w-100 p-2" href={`https://www.tcgplayer.com/product/${card?.productId}`} target="_blank" onclick="dataLayer.push({'event': 'tcgplayerClick', 'tcgplayerUrl': this.getAttribute('href')});"><img src="/vendors/tcgplayer.webp"> <span class="d-none d-lg-inline">TCGPlayer</span></a>
|
||||||
<a class="btn btn-outline-light mb-2 w-100" href={`${ebaySearchUrl(card)}`} target="_blank" onclick="dataLayer.push({'event': 'ebayClick', 'ebayUrl': this.getAttribute('href')});"><span set:html={ebay} /></a>
|
<a class="btn btn-dark mb-2 w-100 p-2" href={`${ebaySearchUrl(card)}`} target="_blank" onclick="dataLayer.push({'event': 'ebayClick', 'ebayUrl': this.getAttribute('href')});"><span set:html={ebay} /></a>
|
||||||
|
<a class="btn btn-dark mb-2 w-100 p-2" href={`${altSearchUrl(card)}`} target="_blank" onclick="dataLayer.push({'event': 'altClick', 'altUrl': this.getAttribute('href')});"><svg width="48" height="20.16" viewBox="0 0 48 20" fill="none"><path d="M14.2761 19.9996H18.5308L11.6934 0.0712891H7.76953L14.2761 19.9996Z" fill="#ffffff"></path><path d="M6.17778 19.9986H6.14536L3.19643 11.2305L0 19.9988L6.17768 19.9989L6.17778 19.9986Z" fill="#ffffff"></path><path d="M24.7842 0H20.6759V19.9661H34.3427V16.5426H24.7842V0Z" fill="#ffffff"></path><path d="M41.6644 3.42355H47.4981V0H31.5033V3.42355H37.5561V19.9661H41.6644V3.42355Z" fill="#ffffff"></path></svg></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-end my-0"><small class="text-body-tertiary">Prices last changed: {timeAgo(calculatedAt)}</small></div>
|
<div class="text-end my-0"><small class="text-body-tertiary">Prices last changed: {timeAgo(calculatedAt)}</small></div>
|
||||||
@@ -227,324 +245,42 @@ const ebaySearchUrl = (card: any) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script is:inline>
|
<script is:inline>
|
||||||
|
|
||||||
async function copyImage(img) {
|
async function copyImage(img) {
|
||||||
|
try {
|
||||||
const canvas = document.createElement("canvas");
|
const canvas = document.createElement("canvas");
|
||||||
const ctx = canvas.getContext("2d");
|
const ctx = canvas.getContext("2d");
|
||||||
|
|
||||||
canvas.width = img.naturalWidth;
|
canvas.width = img.naturalWidth;
|
||||||
canvas.height = img.naturalHeight;
|
canvas.height = img.naturalHeight;
|
||||||
|
|
||||||
// draw the real image pixels
|
|
||||||
ctx.drawImage(img, 0, 0);
|
ctx.drawImage(img, 0, 0);
|
||||||
|
|
||||||
// convert to blob
|
const blob = await new Promise((resolve, reject) => {
|
||||||
canvas.toBlob(async (blob) => {
|
canvas.toBlob(b => b ? resolve(b) : reject(new Error('toBlob failed')), 'image/png');
|
||||||
await navigator.clipboard.write([
|
|
||||||
new ClipboardItem({ "image/png": blob })
|
|
||||||
]);
|
|
||||||
console.log("Copied image via canvas.");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ===== Card Navigation System =====
|
|
||||||
// Only declare class if it hasn't been declared yet (prevents HTMX reload errors)
|
|
||||||
if (typeof window.CardNavigator === 'undefined') {
|
|
||||||
window.CardNavigator = class CardNavigator {
|
|
||||||
constructor() {
|
|
||||||
this.touchStartX = 0;
|
|
||||||
this.touchEndX = 0;
|
|
||||||
this.swipeThreshold = 50; // minimum distance in pixels for a swipe
|
|
||||||
this.loadingMoreCards = false;
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
init() {
|
|
||||||
this.setupEventListeners();
|
|
||||||
this.updateNavigationButtons();
|
|
||||||
this.setupScrollObserver();
|
|
||||||
}
|
|
||||||
|
|
||||||
setupScrollObserver() {
|
|
||||||
// Listen for when new cards are added to the grid
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
this.loadingMoreCards = false;
|
|
||||||
});
|
|
||||||
|
|
||||||
const cardGrid = document.getElementById('cardGrid');
|
|
||||||
if (cardGrid) {
|
|
||||||
observer.observe(cardGrid, {
|
|
||||||
childList: true,
|
|
||||||
subtree: false
|
|
||||||
});
|
});
|
||||||
|
await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
|
||||||
|
showCopyToast('📋 Image copied!', '#198754');
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed:', err);
|
||||||
|
showCopyToast('❌ Copy failed', '#dc3545');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEventListeners() {
|
function showCopyToast(message, color) {
|
||||||
const modal = document.getElementById('cardModal');
|
const toast = document.createElement('div');
|
||||||
if (!modal) return;
|
toast.textContent = message;
|
||||||
|
toast.style.cssText = `
|
||||||
// Keyboard navigation
|
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
|
||||||
document.addEventListener('keydown', (e) => this.handleKeydown(e));
|
background: ${color}; color: white; padding: 10px 20px;
|
||||||
|
border-radius: 8px; font-size: 14px; z-index: 9999;
|
||||||
// Touch/Swipe navigation
|
opacity: 0; transition: opacity 0.2s ease;
|
||||||
modal.addEventListener('touchstart', (e) => this.handleTouchStart(e), false);
|
pointer-events: none;
|
||||||
modal.addEventListener('touchend', (e) => this.handleTouchEnd(e), false);
|
`;
|
||||||
|
document.body.appendChild(toast);
|
||||||
// Button navigation
|
requestAnimationFrame(() => toast.style.opacity = '1');
|
||||||
const prevBtn = document.querySelector('.card-nav-prev');
|
|
||||||
const nextBtn = document.querySelector('.card-nav-next');
|
|
||||||
|
|
||||||
if (prevBtn) prevBtn.addEventListener('click', () => this.navigatePrev());
|
|
||||||
if (nextBtn) nextBtn.addEventListener('click', () => this.navigateNext());
|
|
||||||
}
|
|
||||||
|
|
||||||
handleKeydown(e) {
|
|
||||||
// Only navigate if the modal is visible
|
|
||||||
const modal = document.getElementById('cardModal');
|
|
||||||
if (!modal || !modal.classList.contains('show')) return;
|
|
||||||
|
|
||||||
if (e.key === 'ArrowLeft') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.navigatePrev();
|
|
||||||
} else if (e.key === 'ArrowRight') {
|
|
||||||
e.preventDefault();
|
|
||||||
this.navigateNext();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTouchStart(e) {
|
|
||||||
this.touchStartX = e.changedTouches[0].screenX;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTouchEnd(e) {
|
|
||||||
this.touchEndX = e.changedTouches[0].screenX;
|
|
||||||
this.handleSwipe();
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSwipe() {
|
|
||||||
const diff = this.touchStartX - this.touchEndX;
|
|
||||||
|
|
||||||
// Swipe left = show next card
|
|
||||||
if (diff > this.swipeThreshold) {
|
|
||||||
this.navigateNext();
|
|
||||||
}
|
|
||||||
// Swipe right = show previous card
|
|
||||||
else if (diff < -this.swipeThreshold) {
|
|
||||||
this.navigatePrev();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getVisibleCards() {
|
|
||||||
// Get all card triggers from the current grid
|
|
||||||
return Array.from(document.querySelectorAll('[data-card-id]'));
|
|
||||||
}
|
|
||||||
|
|
||||||
getCurrentCardElement() {
|
|
||||||
const modal = document.getElementById('cardModal');
|
|
||||||
if (!modal) return null;
|
|
||||||
|
|
||||||
const currentCardId = modal.querySelector('.modal-content')?.getAttribute('data-card-id');
|
|
||||||
return currentCardId ? document.querySelector(`[data-card-id="${currentCardId}"]`) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
navigatePrev() {
|
|
||||||
const cards = this.getVisibleCards();
|
|
||||||
const currentCard = this.getCurrentCardElement();
|
|
||||||
|
|
||||||
if (!cards.length || !currentCard) return;
|
|
||||||
|
|
||||||
const currentIndex = cards.indexOf(currentCard);
|
|
||||||
if (currentIndex <= 0) return; // Already at the first card
|
|
||||||
|
|
||||||
const prevCard = cards[currentIndex - 1];
|
|
||||||
this.loadCard(prevCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
navigateNext() {
|
|
||||||
const cards = this.getVisibleCards();
|
|
||||||
const currentCard = this.getCurrentCardElement();
|
|
||||||
|
|
||||||
if (!cards.length || !currentCard) return;
|
|
||||||
|
|
||||||
const currentIndex = cards.indexOf(currentCard);
|
|
||||||
if (currentIndex >= cards.length - 1) {
|
|
||||||
// At the last card, try to load more cards via infinite scroll
|
|
||||||
this.triggerInfiniteScroll();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextCard = cards[currentIndex + 1];
|
|
||||||
this.loadCard(nextCard);
|
|
||||||
}
|
|
||||||
|
|
||||||
triggerInfiniteScroll() {
|
|
||||||
if (this.loadingMoreCards) return; // Already loading
|
|
||||||
|
|
||||||
this.loadingMoreCards = true;
|
|
||||||
|
|
||||||
// Find the infinite scroll trigger element (the "Loading..." div with hx-trigger="revealed")
|
|
||||||
const scrollTrigger = document.querySelector('[hx-trigger="revealed"]');
|
|
||||||
|
|
||||||
if (scrollTrigger) {
|
|
||||||
// Trigger HTMX to load more cards
|
|
||||||
if (typeof htmx !== 'undefined') {
|
|
||||||
htmx.trigger(scrollTrigger, 'revealed');
|
|
||||||
} else {
|
|
||||||
// Fallback: manually call the endpoint
|
|
||||||
const endpoint = scrollTrigger.getAttribute('hx-post') || scrollTrigger.getAttribute('hx-get');
|
|
||||||
if (endpoint) {
|
|
||||||
const formData = new FormData(document.getElementById('searchform'));
|
|
||||||
const currentCards = this.getVisibleCards();
|
|
||||||
const start = (currentCards.length - 1) * 20; // Approximate
|
|
||||||
formData.append('start', start.toString());
|
|
||||||
|
|
||||||
fetch(endpoint, {
|
|
||||||
method: 'POST',
|
|
||||||
body: formData
|
|
||||||
})
|
|
||||||
.then(r => r.text())
|
|
||||||
.then(html => {
|
|
||||||
const grid = document.getElementById('cardGrid');
|
|
||||||
if (grid) {
|
|
||||||
grid.insertAdjacentHTML('beforeend', html);
|
|
||||||
this.waitForNewCards();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for new cards to be added and then navigate
|
|
||||||
this.waitForNewCards();
|
|
||||||
} else {
|
|
||||||
this.loadingMoreCards = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForNewCards() {
|
|
||||||
// Wait up to 3 seconds for new cards to load
|
|
||||||
let attempts = 0;
|
|
||||||
const checkInterval = setInterval(() => {
|
|
||||||
attempts++;
|
|
||||||
const cards = this.getVisibleCards();
|
|
||||||
const currentCardId = document.querySelector('#cardModal .modal-content')?.getAttribute('data-card-id');
|
|
||||||
const currentIndex = cards.findIndex(c => c.getAttribute('data-card-id') === currentCardId);
|
|
||||||
|
|
||||||
// If we have more cards than before, navigate to the next one
|
|
||||||
if (currentIndex >= 0 && currentIndex < cards.length - 1) {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
const nextCard = cards[currentIndex + 1];
|
|
||||||
this.loadingMoreCards = false;
|
|
||||||
this.loadCard(nextCard);
|
|
||||||
} else if (attempts > 30) { // 30 * 100ms = 3 seconds
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
this.loadingMoreCards = false;
|
|
||||||
this.updateNavigationButtons();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
|
|
||||||
loadCard(cardElement) {
|
|
||||||
const hxGet = cardElement.getAttribute('hx-get');
|
|
||||||
if (!hxGet) return;
|
|
||||||
|
|
||||||
// Check if HTMX is available, if not use fetch
|
|
||||||
if (typeof htmx !== 'undefined') {
|
|
||||||
// Use HTMX to load the card
|
|
||||||
htmx.ajax('GET', hxGet, {
|
|
||||||
target: '#cardModal',
|
|
||||||
swap: 'innerHTML',
|
|
||||||
onLoad: () => {
|
|
||||||
this.updateNavigationButtons();
|
|
||||||
// Trigger any analytics event if needed
|
|
||||||
const cardTitle = cardElement.querySelector('#cardImage')?.getAttribute('alt') || 'Unknown Card';
|
|
||||||
if (typeof dataLayer !== 'undefined') {
|
|
||||||
dataLayer.push({
|
|
||||||
'event': 'virtualPageview',
|
|
||||||
'pageUrl': hxGet,
|
|
||||||
'pageTitle': cardTitle,
|
|
||||||
'previousUrl': '/pokemon'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Fallback to native fetch if HTMX not available
|
|
||||||
fetch(hxGet)
|
|
||||||
.then(response => response.text())
|
|
||||||
.then(html => {
|
|
||||||
const modalContent = document.querySelector('#cardModal .modal-dialog');
|
|
||||||
if (modalContent) {
|
|
||||||
modalContent.innerHTML = html;
|
|
||||||
this.updateNavigationButtons();
|
|
||||||
// Trigger any analytics event if needed
|
|
||||||
const cardTitle = cardElement.querySelector('#cardImage')?.getAttribute('alt') || 'Unknown Card';
|
|
||||||
if (typeof dataLayer !== 'undefined') {
|
|
||||||
dataLayer.push({
|
|
||||||
'event': 'virtualPageview',
|
|
||||||
'pageUrl': hxGet,
|
|
||||||
'pageTitle': cardTitle,
|
|
||||||
'previousUrl': '/pokemon'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => console.error('Error loading card:', error));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateNavigationButtons() {
|
|
||||||
const cards = this.getVisibleCards();
|
|
||||||
const currentCard = this.getCurrentCardElement();
|
|
||||||
const currentIndex = cards.indexOf(currentCard);
|
|
||||||
|
|
||||||
const prevBtn = document.querySelector('.card-nav-prev');
|
|
||||||
const nextBtn = document.querySelector('.card-nav-next');
|
|
||||||
|
|
||||||
if (prevBtn) {
|
|
||||||
prevBtn.disabled = currentIndex <= 0;
|
|
||||||
prevBtn.style.opacity = currentIndex <= 0 ? '0.5' : '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (nextBtn) {
|
|
||||||
nextBtn.disabled = currentIndex >= cards.length - 1;
|
|
||||||
nextBtn.style.opacity = currentIndex >= cards.length - 1 ? '0.5' : '1';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} // End of: if (typeof window.CardNavigator === 'undefined')
|
|
||||||
|
|
||||||
// Initialize the card navigator when the modal is shown (only setup once)
|
|
||||||
if (!window.cardNavigatorInitialized) {
|
|
||||||
window.cardNavigatorInitialized = true;
|
|
||||||
|
|
||||||
const modalElement = document.getElementById('cardModal');
|
|
||||||
if (modalElement) {
|
|
||||||
modalElement.addEventListener('shown.bs.modal', () => {
|
|
||||||
if (!window.cardNavigator) {
|
|
||||||
window.cardNavigator = new window.CardNavigator();
|
|
||||||
} else {
|
|
||||||
window.cardNavigator.updateNavigationButtons();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also initialize on first load in case the modal is already visible
|
|
||||||
if (document.readyState === 'loading') {
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// Wait a bit for HTMX to load
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (!window.cardNavigator) {
|
toast.style.opacity = '0';
|
||||||
window.cardNavigator = new window.CardNavigator();
|
toast.addEventListener('transitionend', () => toast.remove());
|
||||||
}
|
}, 2000);
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Wait a bit for HTMX to load if already loaded
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!window.cardNavigator) {
|
|
||||||
window.cardNavigator = new window.CardNavigator();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -138,6 +138,7 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{(start === 0) &&
|
{(start === 0) &&
|
||||||
|
|
||||||
<div id="facetContainer" hx-swap-oob="true">
|
<div id="facetContainer" hx-swap-oob="true">
|
||||||
@@ -164,8 +165,10 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
<div id="totalResults d-none" class="ms-5 text-secondary" hx-swap-oob="true">
|
||||||
<div id="activeFilters" class="mb-2 d-flex align-items-center justify-content-end small" hx-swap-oob="true">
|
{totalHits} {totalHits === 1 ? ' result' : ' results'}
|
||||||
|
</div>
|
||||||
|
<div id="activeFilters" class="d-flex align-items-center small ms-auto" hx-swap-oob="true">
|
||||||
{(Object.entries(filters).length > 0) &&
|
{(Object.entries(filters).length > 0) &&
|
||||||
<span class="me-1">Filtered by:</span>
|
<span class="me-1">Filtered by:</span>
|
||||||
<ul class="list-group list-group-horizontal">
|
<ul class="list-group list-group-horizontal">
|
||||||
@@ -179,7 +182,6 @@ const facets = searchResults.results.slice(1).map((result: any) => {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<script define:vars={{ totalHits, filters, facets }} is:inline>
|
<script define:vars={{ totalHits, filters, facets }} is:inline>
|
||||||
|
|
||||||
// Filter the facet values to make things like Set easier to find
|
// Filter the facet values to make things like Set easier to find
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
---
|
---
|
||||||
|
export const prerender = false;
|
||||||
import Layout from '../layouts/Main.astro';
|
import Layout from '../layouts/Main.astro';
|
||||||
import Search from '../components/Search.astro';
|
import Search from '../components/Search.astro';
|
||||||
import CardGrid from "../components/CardGrid.astro";
|
import CardGrid from "../components/CardGrid.astro";
|
||||||
import NavBar from '../components/NavBar.astro';
|
import NavBar from '../components/NavBar.astro';
|
||||||
|
|
||||||
export const prerender = false;
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout title="Card Search">
|
||||||
<NavBar slot="navbar">
|
<NavBar slot="navbar">
|
||||||
<Search slot="searchInput" />
|
<Search slot="searchInput" />
|
||||||
</NavBar>
|
</NavBar>
|
||||||
|
|||||||
Reference in New Issue
Block a user