mtg-card-explorer / index.html
pearsonkyle's picture
Update index.html
85ef5d5 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Deck Doctor</title>
<meta name="description" content="Advanced MTG card database with similarity search and market prices">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@600;700&display=swap');
:root {
--glass-bg: rgba(0, 0, 0, 0.4);
--glass-bg-dark: rgba(0, 0, 0, 0.6);
--glass-border: rgba(255, 255, 255, 0.08);
--text-primary: rgba(255, 255, 255, 0.9);
--text-secondary: rgba(255, 255, 255, 0.6);
--text-muted: rgba(255, 255, 255, 0.4);
--accent-blue: rgba(102, 126, 234, 0.5);
--border-radius: 16px;
--transition-fast: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
--transition-normal: 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Inter', sans-serif;
background: #0a0a0a;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
min-height: 100vh;
position: relative;
overflow-x: hidden;
overscroll-behavior-y: contain;
touch-action: pan-y;
}
/* Glass overlay panel */
.background-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
z-index: 0;
}
/* Optimized background effects */
body::before {
content: '';
position: fixed;
inset: 0;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
filter: blur(20px) brightness(0.5);
z-index: -1;
opacity: 0.3;
transform: scale(1.1);
will-change: transform;
}
/* Single orb for performance */
.orb {
position: fixed;
width: min(400px, 40vw);
height: min(400px, 40vw);
border-radius: 50%;
background: radial-gradient(circle, rgba(102, 126, 234, 0.4) 0%, transparent 70%);
filter: blur(60px);
opacity: 0.3;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
animation: float 20s infinite ease-in-out;
z-index: 1;
}
@keyframes float {
0%, 100% { transform: translate(-50%, -50%) scale(1); }
50% { transform: translate(-30%, -60%) scale(1.2); }
}
/* Disable complex animations on mobile */
@media (max-width: 768px) {
.orb { animation: none; opacity: 0.2; }
}
.container {
position: relative;
z-index: 20;
}
/* Consolidated glass styles */
.glass {
background: var(--glass-bg);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
transition: var(--transition-normal);
}
.glass-card {
background: linear-gradient(135deg, rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.6));
backdrop-filter: blur(30px);
-webkit-backdrop-filter: blur(30px);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius);
box-shadow: 0 20px 40px -15px rgba(0, 0, 0, 0.5);
overflow: hidden;
}
/* Optimized search */
.search-container {
position: relative;
max-width: 500px;
margin: 0 auto;
}
.search-input {
width: 100%;
background: var(--glass-bg-dark);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: var(--border-radius);
padding: 16px 20px 16px 50px;
color: white;
font-size: 16px;
transition: var(--transition-fast);
outline: none;
position: relative;
z-index: 5;
min-height: 50px;
}
.search-input::placeholder {
color: var(--text-muted);
}
.search-input:focus {
border-color: var(--accent-blue);
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.search-icon {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
color: var(--text-muted);
pointer-events: none;
font-size: 16px;
z-index: 10;
}
.search-clear {
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
color: var(--text-muted);
cursor: pointer;
padding: 4px;
border-radius: 50%;
transition: var(--transition-fast);
display: none;
}
.search-clear.visible {
display: block;
}
.search-clear:hover {
color: var(--text-primary);
background: rgba(255, 255, 255, 0.1);
}
/* Gallery Header with Filters */
.gallery-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.gallery-title {
font-size: 1.5rem;
font-weight: 600;
color: white;
display: flex;
align-items: center;
gap: 12px;
}
/* Color Filters - Redesigned for gallery section */
.color-filters-inline {
display: flex;
align-items: center;
gap: 8px;
}
.color-filter-label {
font-size: 0.875rem;
font-weight: 500;
color: var(--text-secondary);
margin-right: 4px;
}
.color-filter-btn {
width: 36px;
height: 36px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: var(--transition-fast);
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 13px;
position: relative;
background: none;
outline: none;
}
.color-filter-btn:hover {
transform: scale(1.15);
box-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
}
.color-filter-btn.active {
border-color: rgba(255, 255, 255, 0.9);
box-shadow: 0 0 20px rgba(255, 255, 255, 0.5);
transform: scale(1.15);
}
.color-filter-btn::after {
content: '';
position: absolute;
inset: 2px;
border-radius: 50%;
transition: var(--transition-fast);
}
/* Clear filters button */
.clear-filters-btn {
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.4);
color: rgb(248, 113, 113);
padding: 6px 12px;
border-radius: 8px;
font-size: 0.75rem;
cursor: pointer;
transition: var(--transition-fast);
display: none;
}
.clear-filters-btn.visible {
display: inline-flex;
align-items: center;
gap: 4px;
}
.clear-filters-btn:hover {
background: rgba(239, 68, 68, 0.3);
border-color: rgba(239, 68, 68, 0.6);
}
/* Simplified autocomplete */
.autocomplete-dropdown {
position: absolute;
top: calc(100% + 8px);
left: 0;
right: 0;
background: var(--glass-bg-dark);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 12px;
max-height: 300px;
overflow-y: auto;
z-index: 1000;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: var(--transition-fast);
}
.autocomplete-dropdown.visible {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.autocomplete-item {
padding: 12px 16px;
color: var(--text-secondary);
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
transition: var(--transition-fast);
display: flex;
align-items: center;
}
.autocomplete-item:hover,
.autocomplete-item.selected {
background: rgba(102, 126, 234, 0.2);
color: white;
}
.autocomplete-loading,
.autocomplete-empty {
padding: 16px;
text-align: center;
color: var(--text-muted);
font-size: 14px;
}
.autocomplete-hint {
padding: 12px 16px;
background: rgba(102, 126, 234, 0.1);
border-top: 1px solid rgba(255, 255, 255, 0.1);
color: var(--text-secondary);
font-size: 13px;
display: flex;
align-items: center;
gap: 8px;
}
/* Headers */
.header-text {
font-family: 'Space Grotesk', sans-serif;
font-weight: 700;
background: linear-gradient(135deg, #fff 0%, rgba(255, 255, 255, 0.7) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
font-size: clamp(2.5rem, 10vw, 6rem);
letter-spacing: -1px;
line-height: 1;
}
.subheader-text {
color: var(--text-secondary);
font-weight: 300;
letter-spacing: 2px;
text-transform: uppercase;
font-size: clamp(0.75rem, 2vw, 0.875rem);
}
/* Search Results Header */
.search-results-header {
background: var(--glass-bg-dark);
backdrop-filter: blur(20px);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 16px 20px;
margin-bottom: 24px;
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
}
.search-results-title {
font-size: 1.25rem;
font-weight: 600;
color: white;
display: flex;
align-items: center;
gap: 12px;
}
.search-query {
color: rgba(102, 126, 234, 1);
font-weight: 400;
}
.results-count {
color: var(--text-muted);
font-size: 0.875rem;
}
/* Search Results Filters */
.search-results-filters {
display: flex;
align-items: center;
gap: 8px;
}
/* Optimized buttons */
.glass-button {
background: var(--glass-bg);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
color: var(--text-primary);
padding: 12px 24px;
border-radius: 12px;
font-weight: 500;
transition: var(--transition-fast);
cursor: pointer;
font-size: 0.9rem;
white-space: nowrap;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.glass-button:hover:not(:disabled) {
background: rgba(0, 0, 0, 0.5);
border-color: rgba(255, 255, 255, 0.2);
transform: translateY(-2px);
}
.glass-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.glass-button-small {
padding: 6px 12px;
font-size: 0.8rem;
min-height: 32px;
}
/* Oracle text search buttons */
.oracle-search-btn {
background: rgba(102, 126, 234, 0.2);
border: 1px solid rgba(102, 126, 234, 0.4);
color: rgba(102, 126, 234, 1);
padding: 12px 16px;
border-radius: 12px;
font-size: 0.85rem;
cursor: pointer;
transition: var(--transition-fast);
display: block;
margin: 8px 0;
text-align: left;
width: 100%;
word-wrap: break-word;
line-height: 1.5;
white-space: normal;
overflow-wrap: break-word;
word-break: break-word;
}
.oracle-search-btn:hover {
background: rgba(102, 126, 234, 0.3);
border-color: rgba(102, 126, 234, 0.6);
color: white;
transform: translateY(-1px);
}
.oracle-search-btn i {
margin-right: 8px;
flex-shrink: 0;
}
/* Mobile optimization for oracle search buttons */
@media (max-width: 768px) {
.oracle-search-btn {
font-size: 0.8rem;
padding: 10px 14px;
margin: 6px 0;
line-height: 1.4;
display: flex;
align-items: flex-start;
}
.oracle-search-btn i {
margin-right: 8px;
font-size: 0.8rem;
margin-top: 0.1em;
}
}
@media (max-width: 480px) {
.oracle-search-btn {
font-size: 0.75rem;
padding: 10px 12px;
margin: 5px 0;
line-height: 1.4;
border-radius: 10px;
}
.oracle-search-btn i {
margin-right: 6px;
font-size: 0.75rem;
}
}
@media (max-width: 360px) {
.oracle-search-btn {
font-size: 0.7rem;
padding: 8px 10px;
margin: 4px 0;
}
}
/* Card container */
.card-3d-container {
perspective: 2000px;
width: 100%;
max-width: min(420px, 90vw);
margin: 0 auto;
position: relative;
user-select: none;
}
.card-3d {
width: 100%;
aspect-ratio: 0.72;
transition: transform 0.8s cubic-bezier(0.4, 0, 0.2, 1);
transform-style: preserve-3d;
position: relative;
}
.card-face {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 18px;
overflow: hidden;
box-shadow: 0 20px 60px -20px rgba(0, 0, 0, 0.5);
}
.card-face-back {
transform: rotateY(180deg);
}
/* Responsive grids */
.responsive-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: 1fr;
}
@media (min-width: 1024px) {
.responsive-grid {
grid-template-columns: 1fr 1fr;
}
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 0.75rem;
}
.stat-box {
background: var(--glass-bg-dark);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 1rem;
transition: var(--transition-fast);
}
.stat-box:hover {
transform: translateY(-2px);
}
/* Info styles */
.info-label {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
margin-bottom: 0.5rem;
}
.info-value {
color: var(--text-primary);
font-weight: 500;
font-size: 1.1rem;
}
/* Price section */
.price-section {
background: var(--glass-bg-dark);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 1rem;
margin-bottom: 1rem;
}
.price-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 0.75rem;
margin-top: 0.75rem;
}
.price-item {
text-align: center;
}
.price-value {
font-size: 1.25rem;
font-weight: 600;
color: var(--text-primary);
}
.price-label {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
margin-top: 0.25rem;
}
/* Legality badges */
.legality-badge {
padding: 6px 12px;
border-radius: 8px;
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
display: inline-flex;
align-items: center;
gap: 4px;
}
.legality-legal {
background: rgba(16, 185, 129, 0.2);
border: 1px solid rgba(16, 185, 129, 0.4);
color: rgb(52, 211, 153);
}
.legality-not-legal {
background: rgba(148, 163, 184, 0.2);
border: 1px solid rgba(148, 163, 184, 0.4);
color: rgb(148, 163, 184);
}
.legality-banned {
background: rgba(239, 68, 68, 0.2);
border: 1px solid rgba(239, 68, 68, 0.4);
color: rgb(248, 113, 113);
}
/* Enhanced Gallery */
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1.5rem;
}
@media (max-width: 768px) {
.gallery-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
}
.gallery-header {
flex-direction: column;
align-items: flex-start;
}
}
@media (max-width: 480px) {
.gallery-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}
.gallery-item {
position: relative;
border-radius: 12px;
overflow: hidden;
transition: var(--transition-fast);
background: var(--glass-bg);
border: 1px solid var(--glass-border);
}
.gallery-item:hover {
transform: translateY(-4px) scale(1.02);
}
/* Enhanced gallery overlay */
.gallery-overlay {
position: absolute;
inset: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.95) 0%, rgba(0, 0, 0, 0.7) 50%, transparent 100%);
opacity: 0;
transition: var(--transition-fast);
padding: 1rem;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.gallery-item:hover .gallery-overlay {
opacity: 1;
}
.gallery-overlay-content {
space-y: 1rem;
}
.gallery-mana-cost {
display: flex;
align-items: center;
gap: 4px;
margin-bottom: 8px;
}
.mana-symbol {
width: 16px;
height: 16px;
border-radius: 50%;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: bold;
color: white;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.5);
}
.gallery-actions {
display: flex;
gap: 8px;
margin-top: 12px;
}
.gallery-btn {
background: rgba(0, 0, 0, 0.8);
border: 1px solid rgba(255, 255, 255, 0.2);
color: white;
padding: 6px 12px;
border-radius: 6px;
font-size: 0.75rem;
cursor: pointer;
transition: var(--transition-fast);
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 4px;
}
.gallery-btn:hover {
background: rgba(102, 126, 234, 0.8);
border-color: rgba(102, 126, 234, 1);
}
.gallery-btn-view {
background: rgba(102, 126, 234, 0.8);
}
.gallery-btn-view:hover {
background: rgba(102, 126, 234, 1);
transform: translateY(-1px);
}
/* Loading */
.loading-ring {
width: 48px;
height: 48px;
border: 3px solid rgba(255, 255, 255, 0.1);
border-top-color: rgba(102, 126, 234, 0.8);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* Toast */
.toast {
position: fixed;
top: 20px;
right: 20px;
z-index: 1100;
padding: 12px 16px;
border-radius: 8px;
font-size: 14px;
transition: var(--transition-fast);
transform: translateX(400px);
}
.toast.show {
transform: translateX(0);
}
.toast.error {
background: rgba(239, 68, 68, 0.9);
color: white;
}
.toast.success {
background: rgba(16, 185, 129, 0.9);
color: white;
}
/* Price tag */
.price-tag {
background: linear-gradient(135deg, rgba(16, 185, 129, 0.9), rgba(5, 150, 105, 0.9));
color: white;
padding: 6px 12px;
border-radius: 8px;
font-weight: 600;
font-size: 0.875rem;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.3);
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.2);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.3);
}
/* Utilities */
.hidden { display: none !important; }
.line-clamp-2 { display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.line-clamp-3 { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
/* Focus styles */
:focus-visible {
outline: 2px solid var(--accent-blue);
outline-offset: 2px;
}
/* Selection */
::selection {
background: rgba(102, 126, 234, 0.3);
color: white;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
</style>
</head>
<body>
<!-- Background elements -->
<div class="background-overlay"></div>
<div class="orb" aria-hidden="true"></div>
<!-- Toast notifications -->
<div id="toast" class="toast"></div>
<div class="container mx-auto py-4 md:py-8 px-4">
<!-- Header -->
<header class="text-center mb-8 md:mb-12">
<h1 class="header-text text-4xl md:text-6xl lg:text-8xl mb-4 md:mb-6">
Deck Doctor
</h1>
<div class="flex justify-center gap-5 md:gap-8 text-xs md:text-sm flex-wrap">
<span class="text-white/40"><i class="fas fa-shield-alt mr-2"></i>Card Search</span>
<span class="text-white/40"><i class="fas fa-chart-line mr-2"></i>Deck Analysis</span>
<span class="text-white/40"><i class="fas fa-search mr-2"></i>Smart Discovery</span>
</div>
<!-- Search -->
<div class="search-container mt-8">
<i class="fas fa-search search-icon"></i>
<textarea
id="search-input"
class="search-input"
placeholder="Search for cards, terms or paste a deck..."
autocomplete="off"
rows="1"
oninput="autoResize(this)"
></textarea>
<button class="search-clear" id="search-clear" aria-label="Clear">
<i class="fas fa-times"></i>
</button>
<div class="autocomplete-dropdown" id="autocomplete"></div>
</div>
</header>
<!-- Main Card Display -->
<div id="main-card-section" class="responsive-grid max-w-7xl mx-auto">
<!-- Card Display -->
<div class="flex flex-col items-center">
<div class="card-3d-container mb-4">
<div class="card-3d" id="card-3d">
<div class="card-face">
<div id="card-image" class="w-full h-full bg-gray-900 flex items-center justify-center">
<div class="text-center glass-card p-8">
<i class="fas fa-cube text-5xl text-white/30 mb-4"></i>
<p class="text-white/60">Initializing...</p>
</div>
</div>
<div id="face-indicator" class="absolute top-4 right-4 price-tag hidden">Face 1/2</div>
</div>
</div>
</div>
<!-- Controls -->
<div class="flex gap-2 mb-4">
<button id="random-btn" class="glass-button">
<i class="fas fa-shuffle mr-2"></i>Random
</button>
</div>
<!-- Face Navigation -->
<div id="face-nav" class="hidden glass-card px-4 py-2 flex items-center gap-4">
<button onclick="switchFace(-1)" class="text-white/60 hover:text-white">
<i class="fas fa-chevron-left"></i>
</button>
<span id="face-counter" class="text-white font-medium">1/2</span>
<button onclick="switchFace(1)" class="text-white/60 hover:text-white">
<i class="fas fa-chevron-right"></i>
</button>
</div>
</div>
<!-- Info Panel -->
<div id="info-panel" class="space-y-4">
<div class="glass-card p-4 md:p-5">
<h2 id="card-name" class="text-2xl font-semibold text-white mb-1">No Card Selected</h2>
<div id="card-type" class="text-white/60 mb-4"></div>
<!-- Oracle Text Search Section -->
<div id="oracle-search-section" class="mb-4">
<h3 class="info-label">Search Card Mechanics</h3>
<div id="oracle-search-buttons" class="space-y-2"></div>
</div>
<!-- Legalities -->
<div class="mb-4">
<h3 class="info-label mb-2">Format Legality</h3>
<div id="legalities" class="flex flex-wrap gap-1"></div>
</div>
<!-- Rankings -->
<div class="stat-box mb-4">
<p class="info-label">Rankings</p>
<div id="rankings" class="info-value"></div>
</div>
<!-- Prices -->
<div class="price-section">
<h3 class="info-label">Market Prices</h3>
<div id="prices" class="price-grid"></div>
</div>
<!-- Links -->
<div>
<h3 class="info-label mb-2">Purchase & Resources</h3>
<div id="links" class="flex flex-wrap gap-2"></div>
</div>
</div>
</div>
</div>
<!-- Gallery Section with Filters -->
<div id="gallery-section" class="mt-12 hidden">
<div class="gallery-header max-w-7xl mx-auto">
<h2 id="gallery-header" class="gallery-title">Similar Cards</h2>
<div class="color-filters-inline">
<span class="color-filter-label">Filter:</span>
<div id="gallery-color-filters" class="flex gap-2"></div>
<button id="clear-gallery-filters" class="clear-filters-btn" onclick="clearGalleryFilters()">
<i class="fas fa-times"></i>
Clear
</button>
</div>
</div>
<div id="gallery" class="gallery-grid max-w-7xl mx-auto"></div>
</div>
<!-- Search Results Section (Initially Hidden) -->
<div id="search-results-section" class="hidden">
<div class="search-results-header max-w-7xl mx-auto">
<div class="search-results-title">
<i class="fas fa-search"></i>
<span>Search: <span class="search-query" id="search-query-text"></span></span>
</div>
<div class="flex items-center gap-4">
<div class="results-count" id="results-count"></div>
<div class="search-results-filters">
<div id="search-color-filters" class="flex gap-2"></div>
<button id="clear-search-filters" class="clear-filters-btn" onclick="clearSearchFilters()">
<i class="fas fa-times"></i>
Clear
</button>
</div>
</div>
</div>
<div id="search-results" class="gallery-grid max-w-7xl mx-auto"></div>
</div>
<!-- Loading -->
<div id="loading" class="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center hidden z-50">
<div class="glass-card p-6 text-center">
<div class="loading-ring mx-auto mb-3"></div>
<p class="text-white/60">Loading...</p>
</div>
</div>
</div>
<script>
// State
let currentCard = null;
let currentFace = 0;
let isLoading = false;
let searchTimeout = null;
let autocompleteResults = [];
let isSearchMode = false;
let lastSearchQuery = '';
let galleryColors = new Set();
let searchColors = new Set();
let currentGalleryQuery = '';
// Mana color definitions
const manaColors = [
{ symbol: 'W', name: 'White', color: '#FFFBD5', textColor: '#000' },
{ symbol: 'U', name: 'Blue', color: '#0E68AB', textColor: '#fff' },
{ symbol: 'B', name: 'Black', color: '#150B00', textColor: '#fff' },
{ symbol: 'R', name: 'Red', color: '#D3202A', textColor: '#fff' },
{ symbol: 'G', name: 'Green', color: '#00733E', textColor: '#fff' }
];
// DOM Elements
const $ = id => document.getElementById(id);
const searchInput = $('search-input');
const searchClear = $('search-clear');
const autocomplete = $('autocomplete');
const randomBtn = $('random-btn');
const loading = $('loading');
const toast = $('toast');
const mainCardSection = $('main-card-section');
const infoPanel = $('info-panel');
const gallerySection = $('gallery-section');
const searchResultsSection = $('search-results-section');
// Initialize
document.addEventListener('DOMContentLoaded', () => {
createColorFilterButtons('gallery-color-filters', 'gallery');
createColorFilterButtons('search-color-filters', 'search');
fetchRandomCard();
setupEventListeners();
});
// Create color filter buttons for different sections
function createColorFilterButtons(containerId, filterType) {
const container = $(containerId);
if (!container) return;
container.innerHTML = manaColors.map(color => `
<button
class="color-filter-btn"
data-color="${color.symbol}"
data-filter-type="${filterType}"
onclick="toggleColorFilter('${color.symbol}', '${filterType}')"
style="color: ${color.textColor};"
title="Filter by ${color.name}"
>
<span style="
position: absolute;
inset: 0;
background: ${color.color};
border-radius: 50%;
z-index: -1;
"></span>
${color.symbol}
</button>
`).join('');
}
// Toggle color filter for specific section
function toggleColorFilter(colorSymbol, filterType) {
const colorSet = filterType === 'gallery' ? galleryColors : searchColors;
const button = document.querySelector(`[data-color="${colorSymbol}"][data-filter-type="${filterType}"]`);
if (colorSet.has(colorSymbol)) {
colorSet.delete(colorSymbol);
button.classList.remove('active');
} else {
colorSet.add(colorSymbol);
button.classList.add('active');
}
// Update clear button visibility
const clearBtn = filterType === 'gallery' ? $('clear-gallery-filters') : $('clear-search-filters');
if (clearBtn) {
clearBtn.classList.toggle('visible', colorSet.size > 0);
}
// Apply filters based on type
if (filterType === 'gallery' && currentCard) {
fetchSimilarCards(currentCard);
} else if (filterType === 'search' && isSearchMode) {
performDeckDoctorSearch(lastSearchQuery);
}
}
// Clear filters for gallery
function clearGalleryFilters() {
galleryColors.clear();
document.querySelectorAll('[data-filter-type="gallery"]').forEach(btn => {
btn.classList.remove('active');
});
$('clear-gallery-filters').classList.remove('visible');
if (currentCard) {
fetchSimilarCards(currentCard);
}
}
// Clear filters for search
function clearSearchFilters() {
searchColors.clear();
document.querySelectorAll('[data-filter-type="search"]').forEach(btn => {
btn.classList.remove('active');
});
$('clear-search-filters').classList.remove('visible');
if (isSearchMode) {
performDeckDoctorSearch(lastSearchQuery);
}
}
// Event Listeners
function setupEventListeners() {
searchInput.addEventListener('input', handleSearch);
searchInput.addEventListener('keydown', handleSearchKey);
searchClear.addEventListener('click', clearSearch);
randomBtn.addEventListener('click', () => {
exitSearchMode();
clearGalleryFilters();
fetchRandomCard();
});
// Close autocomplete on outside click
document.addEventListener('click', e => {
if (!e.target.closest('.search-container')) {
hideAutocomplete();
}
});
// Keyboard shortcuts
document.addEventListener('keydown', e => {
if (e.key === '/' && document.activeElement !== searchInput) {
e.preventDefault();
searchInput.focus();
}
if (e.key === 'Escape') {
searchInput.blur();
hideAutocomplete();
}
});
}
// Auto-resize textarea
function autoResize(textarea) {
textarea.style.height = 'auto';
textarea.style.height = Math.min(textarea.scrollHeight, 200) + 'px';
}
// Search Functions
function handleSearch(e) {
const query = e.target.value.trim();
searchClear.classList.toggle('visible', query.length > 0);
clearTimeout(searchTimeout);
// Check if this looks like a decklist (multiple lines)
const lines = query.split('\n').filter(line => line.trim().length > 0);
if (lines.length > 5) {
// This is likely a decklist - don't show autocomplete
hideAutocomplete();
return;
}
if (query.length < 2) {
hideAutocomplete();
return;
}
searchTimeout = setTimeout(() => fetchAutocomplete(query), 300);
}
function handleSearchKey(e) {
const query = searchInput.value.trim();
const lines = query.split('\n').filter(line => line.trim().length > 0);
if (e.key === 'Enter' && query.length > 0) {
// Handle decklist if more than 5 lines
if (lines.length > 5) {
e.preventDefault();
hideAutocomplete();
processDecklist(query);
return;
}
// Handle single card search
if (!e.shiftKey) { // Allow Shift+Enter for newlines in regular searches
e.preventDefault();
hideAutocomplete();
// Check if user selected from autocomplete or is typing a custom query
if (autocompleteResults.length > 0 && autocompleteResults.includes(query)) {
// Exact match from autocomplete
selectCard(query);
} else {
// Custom search query - use Deck Doctor search
performDeckDoctorSearch(query);
}
}
}
}
function clearSearch() {
searchInput.value = '';
searchClear.classList.remove('visible');
hideAutocomplete();
clearSearchFilters();
if (isSearchMode) {
exitSearchMode();
fetchRandomCard();
}
}
async function fetchAutocomplete(query) {
try {
showAutocompleteLoading();
const response = await fetch(`https://api.scryfall.com/cards/autocomplete?q=${encodeURIComponent(query)}`);
const data = await response.json();
autocompleteResults = data.data || [];
displayAutocomplete(autocompleteResults, query);
} catch (error) {
console.error('Autocomplete error:', error);
hideAutocomplete();
}
}
function displayAutocomplete(results, query) {
let html = '';
if (!results.length) {
html = '';
} else {
html = results.map(name =>
`<div class="autocomplete-item" onclick="selectCard('${name.replace(/'/g, "\\'")}')">${name}</div>`
).join('');
}
// Add hint for custom search
html += `<div class="autocomplete-hint">
<i class="fas fa-lightbulb"></i>
<span>Press Enter to search cards for "${query}".</span>
</div>`;
autocomplete.innerHTML = html;
showAutocomplete();
}
function showAutocompleteLoading() {
autocomplete.innerHTML = '<div class="autocomplete-loading"><i class="fas fa-spinner fa-spin mr-2"></i>Searching...</div>';
showAutocomplete();
}
function showAutocomplete() {
autocomplete.classList.add('visible');
}
function hideAutocomplete() {
autocomplete.classList.remove('visible');
}
async function selectCard(name) {
hideAutocomplete();
searchInput.value = name;
searchClear.classList.add('visible');
setLoading(true);
exitSearchMode();
try {
const response = await fetch(`https://api.scryfall.com/cards/named?exact=${encodeURIComponent(name)}`);
const card = await response.json();
displayCard(card);
showToast('Card loaded', 'success');
} catch (error) {
showToast('Failed to load card', 'error');
} finally {
setLoading(false);
}
}
// Deck Doctor Search with color filters
async function performDeckDoctorSearch(query) {
setLoading(true);
lastSearchQuery = query;
try {
// Build URL with colors if selected
let url = `https://api.deck.doctor/v1/mtg/search?q=${encodeURIComponent(query)}&topk=20&price_threshold=0`;
// Add color filters for search results
searchColors.forEach(color => {
url += `&colors=${color}`;
});
const response = await fetch(url);
const data = await response.json();
if (data && data.length > 0) {
const colorFilterText = searchColors.size > 0 ? ` (${Array.from(searchColors).join('')})` : '';
enterSearchMode(query + colorFilterText, data);
showToast(`Found ${data.length} cards`, 'success');
} else {
showToast('No cards found', 'error');
}
} catch (error) {
console.error('Search error:', error);
showToast('Search failed', 'error');
} finally {
setLoading(false);
}
}
// Process decklist
async function processDecklist(decklistText) {
setLoading(true);
showToast('Processing decklist...', 'success');
try {
// Parse decklist - extract card names (simple parsing)
const lines = decklistText.split('\n')
.filter(line => line.trim().length > 0)
.map(line => {
// Remove quantity numbers and set codes if present
return line.replace(/^\d+\s*/, '') // Remove leading numbers
.replace(/\s*\(.*?\)\s*$/, '') // Remove set codes in parentheses
.replace(/\s*\*.*?\*\s*$/, '') // Remove any *footnotes*
.trim();
})
.filter(name => name.length > 0);
if (lines.length === 0) {
showToast('No valid card names found in decklist', 'error');
return;
}
// Search for each card in the decklist
const deckCards = [];
for (const cardName of lines) {
try {
const response = await fetch(`https://api.scryfall.com/cards/named?exact=${encodeURIComponent(cardName)}`);
if (response.ok) {
const card = await response.json();
deckCards.push([card, 1.0]); // Use 1.0 as similarity score for decklist items
}
} catch (error) {
console.warn(`Could not find card: ${cardName}`);
}
}
if (deckCards.length > 0) {
enterSearchMode(`Decklist (${deckCards.length} cards)`, deckCards);
showToast(`Loaded ${deckCards.length} cards from decklist`, 'success');
} else {
showToast('No valid cards found in decklist', 'error');
}
} catch (error) {
console.error('Decklist processing error:', error);
showToast('Failed to process decklist', 'error');
} finally {
setLoading(false);
}
}
// Search Mode Management
function enterSearchMode(query, results) {
isSearchMode = true;
// Hide main card display
mainCardSection.classList.add('hidden');
gallerySection.classList.add('hidden');
// Show search results
searchResultsSection.classList.remove('hidden');
$('search-query-text').textContent = query;
$('results-count').textContent = `${results.length} results`;
// Display results
displaySearchResults(results);
}
function exitSearchMode() {
isSearchMode = false;
// Show main card display
mainCardSection.classList.remove('hidden');
// Hide search results
searchResultsSection.classList.add('hidden');
// Clear search filters when exiting search mode
clearSearchFilters();
}
function displaySearchResults(results) {
const searchResultsContainer = $('search-results');
searchResultsContainer.innerHTML = results.map(([card, score]) => `
<div class="gallery-item">
<img src="${card.image_uris?.normal || ''}" alt="${card.name}" class="w-full h-full object-cover">
${card.prices?.usd ? `<div class="absolute bottom-2 left-2 price-tag text-xs">${parseFloat(card.prices.usd).toFixed(2)}</div>` : ''}
<div class="gallery-overlay">
<div class="gallery-overlay-content">
<h4 class="font-semibold text-white line-clamp-2">${card.name}</h4>
<p class="text-xs text-white/60 mt-1">${card.type_line}</p>
${score < 1.0 ? `<p class="text-xs text-white/40 mt-1">${Math.round(score * 100)}% match</p>` :
score === 1.0 ? `<p class="text-xs text-white/40 mt-1">From decklist</p>` : ''}
<div class="gallery-actions">
<button onclick="loadCardFromSearch('${card.id}')" class="gallery-btn gallery-btn-view">
<i class="fas fa-eye"></i>
View Card
</button>
${card.purchase_uris?.tcgplayer ? `
<a href="${card.purchase_uris.tcgplayer}" target="_blank" class="gallery-btn">
<i class="fas fa-shopping-cart"></i>
Buy
</a>
` : ''}
</div>
</div>
</div>
</div>
`).join('');
}
async function loadCardFromSearch(id) {
setLoading(true);
try {
const response = await fetch(`https://api.scryfall.com/cards/${id}`);
const card = await response.json();
// Exit search mode and display the card
exitSearchMode();
displayCard(card);
// Clear search input
searchInput.value = '';
searchClear.classList.remove('visible');
window.scrollTo({ top: 0, behavior: 'smooth' });
showToast('Card loaded successfully', 'success');
} catch (error) {
showToast('Failed to load card', 'error');
} finally {
setLoading(false);
}
}
// Card Functions
async function fetchRandomCard() {
if (isLoading) return;
setLoading(true);
try {
const response = await fetch('https://api.scryfall.com/cards/random?q=is%3Acommander');
const card = await response.json();
displayCard(card);
} catch (error) {
showToast('Failed to fetch card', 'error');
} finally {
setLoading(false);
}
}
function displayCard(card) {
currentCard = card;
currentFace = 0;
// Update background
if (card.image_uris?.art_crop) {
document.body.style.setProperty('--bg-image', `url('${card.image_uris.art_crop}')`);
document.body.style.backgroundImage = `url('${card.image_uris.art_crop}')`;
}
// Display card face
displayCardFace(card, 0);
// Update info
updateCardInfo(card);
// Fetch similar cards
fetchSimilarCards(card);
// Update gallery header back to "Similar Cards"
$('gallery-header').textContent = 'Similar Cards';
}
function displayCardFace(card, faceIndex) {
const hasFaces = card.card_faces?.length > 1;
const face = hasFaces ? card.card_faces[faceIndex] : card;
const imageUris = face.image_uris || card.image_uris;
// Update image
if (imageUris?.large) {
$('card-image').innerHTML = `<img src="${imageUris.large}" alt="${face.name}" class="w-full h-full object-contain">`;
}
// Update face navigation
if (hasFaces) {
$('face-nav').classList.remove('hidden');
$('face-indicator').classList.remove('hidden');
$('face-indicator').textContent = `Face ${faceIndex + 1}/${card.card_faces.length}`;
$('face-counter').textContent = `${faceIndex + 1}/${card.card_faces.length}`;
} else {
$('face-nav').classList.add('hidden');
$('face-indicator').classList.add('hidden');
}
// Update card info
$('card-name').textContent = face.name || card.name;
$('card-type').innerHTML = `<i class="fas fa-layer-group mr-2"></i>${face.type_line || card.type_line}`;
}
// Oracle text search functionality
function createOracleSearchButtons(oracleText) {
if (!oracleText) return '';
const cardName = currentCard.name;
const shortName = cardName.includes(',') ? cardName.split(',')[0].trim() : cardName;
// Split oracle text into meaningful lines
const lines = oracleText.split('\n').filter(line => line.trim().length > 0);
const buttons = [];
lines.forEach((line) => {
let cleanedLine = line.trim();
if (cleanedLine.length > 10) {
// Replace card names with "this card" (case insensitive)
cleanedLine = cleanedLine.replace(new RegExp(cardName, 'gi'), 'this card');
if (shortName !== cardName) {
cleanedLine = cleanedLine.replace(new RegExp(shortName, 'gi'), 'this card');
}
// Show full text in buttons now
buttons.push(`
<button class="oracle-search-btn" onclick="searchOracleText('${cleanedLine.replace(/'/g, "\\'")}')">
<i class="fas fa-search mr-2"></i>
${cleanedLine}
</button>
`);
}
});
return buttons.join('');
}
async function searchOracleText(searchTerm) {
searchInput.value = searchTerm;
searchClear.classList.add('visible');
await performDeckDoctorSearch(searchTerm);
}
// Mana cost display helper
function formatManaCost(manaCost) {
if (!manaCost) return '';
const symbols = manaCost.match(/\{[^}]+\}/g) || [];
return symbols.map(symbol => {
const clean = symbol.replace(/[{}]/g, '');
let color = '';
let bgColor = '';
switch(clean) {
case 'W': color = 'white'; bgColor = '#FFFBD5'; break;
case 'U': color = 'blue'; bgColor = '#0E68AB'; break;
case 'B': color = 'black'; bgColor = '#150B00'; break;
case 'R': color = 'red'; bgColor = '#D3202A'; break;
case 'G': color = 'green'; bgColor = '#00733E'; break;
case 'C': color = 'colorless'; bgColor = '#A89B9A'; break;
default: color = 'generic'; bgColor = '#CAC5C0'; break;
}
return `<span class="mana-symbol" style="background-color: ${bgColor}">${clean}</span>`;
}).join('');
}
function updateCardInfo(card) {
// Oracle text search buttons
const hasFaces = card.card_faces?.length > 1;
const face = hasFaces ? card.card_faces[currentFace] : card;
const oracleText = face.oracle_text || card.oracle_text || '';
const oracleSection = $('oracle-search-section');
if (oracleText) {
$('oracle-search-buttons').innerHTML = createOracleSearchButtons(oracleText);
oracleSection.style.display = 'block';
// Add container class for better mobile layout
$('oracle-search-buttons').classList.add('space-y-2');
} else {
oracleSection.style.display = 'none';
}
// Legalities
const formats = [
{ key: 'standard', name: 'Standard' },
{ key: 'modern', name: 'Modern' },
{ key: 'legacy', name: 'Legacy' },
{ key: 'commander', name: 'Commander' }
];
$('legalities').innerHTML = formats.map(format => {
const status = card.legalities?.[format.key];
if (!status) return '';
const isLegal = status === 'legal';
return `<div class="legality-badge ${isLegal ? 'legality-legal' : status === 'banned' ? 'legality-banned' : 'legality-not-legal'}">
<i class="fas fa-${isLegal ? 'check' : 'times'}"></i>
<span>${format.name}</span>
</div>`;
}).join('');
// Rankings
$('rankings').innerHTML = card.edhrec_rank ?
`#${card.edhrec_rank.toLocaleString()} <span class="text-xs text-white/60">EDH Rank</span>` : '—';
// Prices
const prices = [];
if (card.prices?.usd) prices.push({
label: 'USD',
value: `${parseFloat(card.prices.usd).toFixed(2)}`,
url: card.purchase_uris?.tcgplayer
});
if (card.prices?.usd_foil) prices.push({
label: 'Foil',
value: `${parseFloat(card.prices.usd_foil).toFixed(2)}`,
url: card.purchase_uris?.tcgplayer
});
$('prices').innerHTML = prices.length ? prices.map(p =>
`<a href="${p.url || '#'}" target="_blank" class="price-item" ${p.url ? '' : 'style="cursor: default;"'}>
<div class="price-value">${p.value}</div>
<div class="price-label">${p.label}</div>
</a>`
).join('') : '<div class="text-center text-white/40">No pricing data</div>';
// Links
const links = [];
if (card.scryfall_uri) links.push({ name: 'Scryfall', url: card.scryfall_uri, icon: 'search' });
// Purchase links with fallbacks
if (card.purchase_uris?.tcgplayer) {
links.push({ name: 'TCGPlayer', url: card.purchase_uris.tcgplayer, icon: 'shopping-cart' });
} else if (card.purchase_uris?.cardmarket) {
links.push({ name: 'Cardmarket', url: card.purchase_uris.cardmarket, icon: 'shopping-cart' });
}
if (card.purchase_uris?.cardhoarder) {
links.push({ name: 'Cardhoarder', url: card.purchase_uris.cardhoarder, icon: 'shopping-cart' });
}
if (card.related_uris?.edhrec) links.push({ name: 'EDHREC', url: card.related_uris.edhrec, icon: 'chart-bar' });
$('links').innerHTML = links.map(link =>
`<a href="${link.url}" target="_blank" class="glass-button">
<i class="fas fa-${link.icon} mr-2"></i>${link.name}
</a>`
).join('');
}
// Build card query string matching Python function
function buildCardQueryString(card) {
// Extract card data
const name = card.name || '';
const manaCost = card.mana_cost || '';
const cmc = card.cmc || 0;
const colors = card.colors || [];
const types = card.type_line ? card.type_line.split('—')[0].trim().split(' ') : [];
const subtypes = card.type_line && card.type_line.includes('—') ?
card.type_line.split('—')[1].trim().split(' ') : [];
const oracleText = card.oracle_text || '';
const power = card.power || null;
const toughness = card.toughness || null;
// Color replacement
let colorString = colors.join(' ')
.replace(/W/g, 'Plains')
.replace(/B/g, 'Swamp')
.replace(/U/g, 'Island')
.replace(/R/g, 'Mountain')
.replace(/G/g, 'Forest');
// Clean color text
colorString = colorString.replace(/\}\{/g, ',').replace(/\{/g, '').replace(/\}/g, '');
// Clean mana cost
let cleanManaCost = manaCost.replace(/\{/g, '').replace(/\}/g, '').replace(/ /g, '').toLowerCase();
if (cmc) {
cleanManaCost += ` (${parseInt(cmc)})`;
}
// Clean oracle text
let cleanText = oracleText
.replace(/•/g, '--')
.replace(/\n/g, '. ')
.replace(/\{/g, '')
.replace(/\}/g, '')
.replace(/\.\./g, '.')
.replace(/ /g, ' ')
.replace(/\u2014/g, '--')
.replace(/ \u2022/g, ':');
// Replace name in text
let cardName = name;
if (name.includes(',')) {
const altName = name.split(',')[0].trim();
cleanText = cleanText.replace(new RegExp(altName, 'g'), 'this card');
}
const subtypeString = subtypes.join(' ');
const typeString = types.join(' ');
// Build card string based on type
let cardStr = '';
if (types.includes('Creature')) {
cardStr = `${cardName}, ${cleanManaCost}, ${subtypeString}, ${power}/${toughness}, ${cleanText}`;
} else if (types.includes('Land')) {
cardStr = `${cardName}, ${colorString}, ${cleanText}`;
} else {
cardStr = `${cardName}, ${cleanManaCost}, ${typeString}, ${cleanText}`;
}
// Clean up card string
cardStr = cardStr.trim();
if (cardStr.endsWith(',')) {
cardStr = cardStr.slice(0, -1).trim();
}
// Final cleaning
return cardStr.replace(/, ,/g, ',');
}
async function fetchSimilarCards(card) {
try {
// Build the query string using the same logic as Python
const query = buildCardQueryString(card);
currentGalleryQuery = query;
console.log('Query string:', query); // Debug logging
// Build URL with gallery color filters
let url = `https://api.deck.doctor/v1/mtg/search?q=${encodeURIComponent(query)}&topk=12&price_threshold=0`;
// Add color filters for gallery
galleryColors.forEach(color => {
url += `&colors=${color}`;
});
const response = await fetch(url);
const data = await response.json();
if (data?.length > 0) {
// Filter out the current card more robustly
const filteredResults = data.filter(([c]) => {
// Check multiple identifiers to ensure we exclude the current card
return c.id !== card.id &&
c.name !== card.name &&
(!c.scryfall_uri || c.scryfall_uri !== card.scryfall_uri);
}).slice(0, 11); // Take at most 11 cards after filtering
displayGallery(filteredResults);
$('gallery-section').classList.remove('hidden');
// Update header if filters are active
const colorFilterText = galleryColors.size > 0 ? ` (${Array.from(galleryColors).join('')})` : '';
$('gallery-header').textContent = `Similar Cards${colorFilterText}`;
}
} catch (error) {
console.error('Gallery error:', error);
}
}
function displayGallery(cards) {
$('gallery').innerHTML = cards.map(([card, score]) => `
<div class="gallery-item">
<img src="${card.image_uris?.normal || ''}" alt="${card.name}" class="w-full h-full object-cover">
${card.prices?.usd ? `<div class="absolute top-2 left-2 price-tag text-xs">$${parseFloat(card.prices.usd).toFixed(2)}</div>` : ''}
<div class="gallery-overlay">
<div class="gallery-overlay-content">
<h4 class="font-semibold text-white line-clamp-2 mb-2">${card.name}</h4>
${card.mana_cost ? `<div class="gallery-mana-cost mb-2">${formatManaCost(card.mana_cost)}</div>` : ''}
<p class="text-xs text-white/60 line-clamp-2 mb-2">${card.type_line}</p>
${card.oracle_text ? `<p class="text-xs text-white/50 line-clamp-3 mb-3">${card.oracle_text}</p>` : ''}
<p class="text-xs text-white/40 mb-3">${Math.round(score * 100)}% similar</p>
<div class="gallery-actions">
<button onclick="loadCard('${card.id}')" class="gallery-btn gallery-btn-view">
<i class="fas fa-eye"></i>
View Card
</button>
${card.purchase_uris?.tcgplayer ? `
<a href="${card.purchase_uris.tcgplayer}" target="_blank" class="gallery-btn">
<i class="fas fa-shopping-cart"></i>
TCG
</a>
` : card.purchase_uris?.cardmarket ? `
<a href="${card.purchase_uris.cardmarket}" target="_blank" class="gallery-btn">
<i class="fas fa-shopping-cart"></i>
CM
</a>
` : ''}
</div>
</div>
</div>
</div>
`).join('');
}
async function loadCard(id) {
setLoading(true);
try {
const response = await fetch(`https://api.scryfall.com/cards/${id}`);
const card = await response.json();
displayCard(card);
window.scrollTo({ top: 0, behavior: 'smooth' });
showToast('Card loaded successfully', 'success');
} catch (error) {
showToast('Failed to load card', 'error');
} finally {
setLoading(false);
}
}
function switchFace(direction) {
if (!currentCard?.card_faces) return;
currentFace = (currentFace + direction + currentCard.card_faces.length) % currentCard.card_faces.length;
displayCardFace(currentCard, currentFace);
updateCardInfo(currentCard); // Refresh oracle text buttons for the new face
}
// Utilities
function setLoading(state) {
isLoading = state;
loading.classList.toggle('hidden', !state);
randomBtn.disabled = state;
}
function showToast(message, type = 'info') {
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => toast.classList.remove('show'), 3000);
}
</script>
</body>
</html>