🔧 Gemini AI Integration: - Added Google Gemini API as default OCR service - Auto-configures from GEMINI_AI_API_KEY environment variable - Fixed Puter.js authentication issues - Enhanced OCR settings with connection testing 🎨 Redesigned Scanner Queue: - New thumbnail + content layout with checkbox overlay - Smart quantity management (duplicates increment quantity) - Complete card information display from database - Two-row action layout (primary/secondary actions) - Floating bottom toolbar for bulk actions - Real card images from database �� Enhanced User Experience: - Fixed Canvas2D performance warnings - Better error handling and fallbacks - Improved responsive design - Database confirmation indicators - Professional card scanning workflow 📱 Mobile Ready: - Optimized layouts for mobile scanning - Touch-friendly controls and interactions - Improved visual feedback and status indicators
233 lines
No EOL
6.4 KiB
JavaScript
233 lines
No EOL
6.4 KiB
JavaScript
import { useState, useEffect } from 'react';
|
|
import { parseManaSymbols, getColorSymbol } from '../lib/mana-symbols';
|
|
|
|
/**
|
|
* Individual mana symbol component
|
|
*/
|
|
function ManaSymbol({ symbol, color, name, scryfall_uri, size = 'md', useSVG = false }) {
|
|
const sizeClasses = {
|
|
xs: 'w-3 h-3 text-xs',
|
|
sm: 'w-4 h-4 text-xs',
|
|
md: 'w-5 h-5 text-sm',
|
|
lg: 'w-6 h-6 text-base',
|
|
xl: 'w-8 h-8 text-lg'
|
|
};
|
|
|
|
const isGradient = color.includes('linear-gradient');
|
|
|
|
// If using SVG and we have a Scryfall URI, render the SVG
|
|
if (useSVG && scryfall_uri) {
|
|
return (
|
|
<img
|
|
src={scryfall_uri}
|
|
alt={name}
|
|
title={name}
|
|
className={`${sizeClasses[size]} flex-shrink-0`}
|
|
style={{ filter: 'drop-shadow(0 0 2px rgba(0,0,0,0.3))' }}
|
|
/>
|
|
);
|
|
}
|
|
|
|
// Default circular symbol rendering
|
|
return (
|
|
<span
|
|
className={`inline-flex items-center justify-center rounded-full border border-gray-400 font-bold text-white ${sizeClasses[size]} flex-shrink-0`}
|
|
style={{
|
|
background: isGradient ? color : color,
|
|
color: color === '#FFFBD5' ? '#000' : '#fff',
|
|
textShadow: color === '#FFFBD5' ? 'none' : '0 0 2px rgba(0,0,0,0.8)'
|
|
}}
|
|
title={name}
|
|
>
|
|
{symbol}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Mana cost display component
|
|
* Renders a mana cost string as individual mana symbols
|
|
*/
|
|
export function ManaCost({ cost, size = 'md', className = '', useSVG = false }) {
|
|
const [symbols, setSymbols] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
async function loadSymbols() {
|
|
if (!cost) {
|
|
setSymbols([]);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const parsedSymbols = await parseManaSymbols(cost);
|
|
setSymbols(parsedSymbols);
|
|
} catch (error) {
|
|
console.error('Error parsing mana symbols:', error);
|
|
setSymbols([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
loadSymbols();
|
|
}, [cost]);
|
|
|
|
if (!cost) return null;
|
|
if (loading) {
|
|
return (
|
|
<div className={`flex items-center space-x-1 ${className}`}>
|
|
<div className="w-4 h-4 bg-gray-300 rounded-full animate-pulse"></div>
|
|
</div>
|
|
);
|
|
}
|
|
if (symbols.length === 0) return null;
|
|
|
|
return (
|
|
<div className={`flex items-center space-x-1 ${className}`}>
|
|
{symbols.map((symbolData, index) => (
|
|
<ManaSymbol
|
|
key={`${symbolData.raw}-${index}`}
|
|
symbol={symbolData.symbol}
|
|
color={symbolData.color}
|
|
name={symbolData.name}
|
|
scryfall_uri={symbolData.scryfall_uri}
|
|
size={size}
|
|
useSVG={useSVG}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Color identity display component
|
|
* Shows the color identity of a card using mana symbols
|
|
*/
|
|
export function ColorIdentity({ colors, size = 'sm', className = '', useSVG = false }) {
|
|
if (!colors || colors.length === 0) {
|
|
// Show colorless symbol for cards with no color identity
|
|
const colorlessSymbol = getColorSymbol('C');
|
|
return (
|
|
<div className={`flex items-center space-x-1 ${className}`}>
|
|
<ManaSymbol
|
|
symbol={colorlessSymbol.symbol}
|
|
color={colorlessSymbol.color}
|
|
name={colorlessSymbol.name}
|
|
scryfall_uri={colorlessSymbol.scryfall_uri}
|
|
size={size}
|
|
useSVG={useSVG}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`flex items-center space-x-1 ${className}`}>
|
|
{colors.map((color) => {
|
|
const symbolData = getColorSymbol(color);
|
|
return (
|
|
<ManaSymbol
|
|
key={color}
|
|
symbol={symbolData.symbol}
|
|
color={symbolData.color}
|
|
name={symbolData.name}
|
|
scryfall_uri={symbolData.scryfall_uri}
|
|
size={size}
|
|
useSVG={useSVG}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Single color symbol component for filters
|
|
*/
|
|
export function ColorFilterSymbol({ color, isActive, onClick, size = 'md', useSVG = false }) {
|
|
const symbolData = getColorSymbol(color);
|
|
|
|
return (
|
|
<button
|
|
onClick={() => onClick(color)}
|
|
className={`transition-all duration-200 ${
|
|
isActive
|
|
? 'ring-2 ring-accent-ember scale-110'
|
|
: 'opacity-70 hover:opacity-100 hover:scale-105'
|
|
}`}
|
|
title={symbolData.name}
|
|
>
|
|
<ManaSymbol
|
|
symbol={symbolData.symbol}
|
|
color={symbolData.color}
|
|
name={symbolData.name}
|
|
scryfall_uri={symbolData.scryfall_uri}
|
|
size={size}
|
|
useSVG={useSVG}
|
|
/>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Advanced mana cost component with Scryfall analysis
|
|
*/
|
|
export function AdvancedManaCost({ cost, showAnalysis = false, useSVG = false }) {
|
|
const [analysis, setAnalysis] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
async function loadAnalysis() {
|
|
if (!cost) {
|
|
setAnalysis(null);
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const { getManaCostAnalysis } = await import('../lib/mana-symbols');
|
|
const analysisData = await getManaCostAnalysis(cost);
|
|
setAnalysis(analysisData);
|
|
} catch (error) {
|
|
console.error('Error analyzing mana cost:', error);
|
|
setAnalysis(null);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
loadAnalysis();
|
|
}, [cost]);
|
|
|
|
if (!cost) return null;
|
|
if (loading) {
|
|
return <div className="w-4 h-4 bg-gray-300 rounded-full animate-pulse"></div>;
|
|
}
|
|
if (!analysis) return <ManaCost cost={cost} useSVG={useSVG} />;
|
|
|
|
return (
|
|
<div className="space-y-2">
|
|
<ManaCost cost={analysis.cost} useSVG={useSVG} />
|
|
{showAnalysis && (
|
|
<div className="text-xs text-text-secondary space-y-1">
|
|
<div>CMC: {analysis.cmc}</div>
|
|
{analysis.colors.length > 0 && (
|
|
<div className="flex items-center space-x-1">
|
|
<span>Colors:</span>
|
|
<ColorIdentity colors={analysis.colors} size="xs" useSVG={useSVG} />
|
|
</div>
|
|
)}
|
|
<div className="flex space-x-2">
|
|
{analysis.colorless && <span className="bg-gray-100 text-gray-800 px-1 rounded text-xs">Colorless</span>}
|
|
{analysis.monocolored && <span className="bg-blue-100 text-blue-800 px-1 rounded text-xs">Mono</span>}
|
|
{analysis.multicolored && <span className="bg-purple-100 text-purple-800 px-1 rounded text-xs">Multi</span>}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default ManaCost;
|