deckhearth/components/ManaSymbols.js

234 lines
6.5 KiB
JavaScript
Raw Normal View History

/* eslint-disable @next/next/no-img-element -- Scryfall mana SVG URLs; next/image migration is out of scope. */
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 && (
refactor(design): site-wide sweep — broken Tailwind tokens, rounded corners, SearchBar primitive (#117) (#117) Comprehensive design sweep across the rest of the app following the shipped Liquid Glass + corner-border-light system (#116). ## Three classes of finding ### 1. Broken Tailwind token classes (HIGH — pages were unstyled) The decks / deck-builder / deck-detail cluster relied on Tailwind classes that don't exist in `tailwind.config.js` (no `bg-bg-*`, `text-text-*`, `border-border`, `bg-accent-ember`, `focus:ring-accent-ember`, `hover:bg-accent-ember-dark`). Those classes produced ZERO CSS — backgrounds were transparent, borders invisible, hover states absent. Rewrote with inline `style={{ ... CSS vars ... }}` + the `<Button>` / `<SearchBar>` primitives + `glass-panel` surfaces: - `pages/decks.js` (full page) - `pages/deck/[id].js` (header, stats sidebar, group-by controls, card list) - `pages/deck-builder.js` (loading spinner) - `components/DeckBuilderView.js` (toolbar + main panel) - `components/DeckBuilderCardBrowser.js` (full rewrite; integrated `<SearchBar>` for the card-picker input) - `components/DeckBuilderDeckList.js` (full rewrite) - `components/DeckBuilderStatsBar.js` - `components/ManaSymbolSettings.js` - `components/ManaSymbols.js` (single `text-text-secondary`) - `pages/admin/card-editor.js` cluster was already clean ### 2. Duplicative / stale page searches Replaced raw `<input>` search controls with the `<SearchBar>` primitive (adds clear button, ember focus ring, system-consistent rounded corners). Kept page-specific filter searches (they filter the visible list — distinct from the global TopSearchBar command palette): - `pages/my-cards.js` - `pages/community/collections.js` - `components/CardsPageView.js` - `components/CollectionPageView.js` - `components/DeckBuilderCardBrowser.js` `pages/my-cards.js` filter wrapper also lifted into a `glass-panel` chip instead of a solid `var(--bg-primary)` band. ### 3. Square corners + stale palette in shared views - `components/CollectionPageView.js`: 10 action buttons (`rounded-lg` + `hover:bg-gray-50`) → `rounded-xl` + `nav-item-hover`; 4 filter selects (`focus:ring-purple-500 rounded-lg`) → `.input-field`; view-mode toggle (`bg-white text-gray-900` — invisible in dark mode) → tokenised; SYSTEM badge gradient (`from-blue-500 to-purple-600`) → ember↔flame; tooltip (`bg-gray-900`) → `glass-panel-strong`; search-results dropdown (`bg-white border-gray-200` — invisible in dark mode) → `glass-panel-strong`; Activity / game-count / TCG-game badges palette-aligned. - `components/CardsPageView.js`: "Load More Cards" button (`bg-gradient-to-r from-blue-500 to-purple-600 rounded-lg`) → `<Button variant="primary" size="lg">`. - `components/CollectionsPageView.js`: matching SYSTEM badge + tooltip cleanup. - `components/ShareModal.js`: user-search dropdown (`border-gray-200 hover:bg-gray-50`) and email-invite card moved onto `glass-panel` + `nav-item-hover`; social-share buttons `rounded-lg hover:bg-gray-50` → `rounded-xl nav-item-hover`. - `components/Layout.js`: profile-menu dropdown row (`hover:bg-gray-50 dark:hover:bg-gray-700`) → `nav-item-hover`. - `components/CardItem.js`: bulk-select checkbox `focus:ring-purple-500` → ember. ### 4. `dark:` modifier classes (broken with `[data-theme]` theming) This app uses `[data-theme="dark"]` CSS selector theming, not Tailwind's `class` strategy, so `dark:bg-green-900/20` etc. produced no CSS in dark mode. Affected alerts on `pages/settings.js` and `pages/profile.js` — replaced with `glass-panel` + semantic border colour (flame for success, #dc2626 for error). `pages/settings.js` sidebar nav also moved off its hardcoded full-ember fill onto the system `nav-item` / `nav-item-active` / `nav-item-hover` pattern for consistency with the global sidebar. ## Verification - `npm run build` — green (Next 16 + Turbopack) - `npm run lint` — 0 errors, 1 unrelated pre-existing warning - `npm run test:run` — 113/113 pass (no test changes needed) Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-04 15:06:22 -04:00
<div className="text-xs space-y-1" style={{ color: 'var(--text-secondary)' }}>
<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;