feat: Add support for card finishes (foil/normal) from input parsing through to UI display.

This commit is contained in:
2025-12-16 13:14:02 +01:00
parent 260920184d
commit 8433d02e5b
5 changed files with 48 additions and 15 deletions

View File

@@ -9,6 +9,8 @@ interface PackCardProps {
}
const ListItem: React.FC<{ card: DraftCard }> = ({ card }) => {
const isFoil = (card: DraftCard) => card.finish === 'foil';
const getRarityColorClass = (rarity: string) => {
switch (rarity) {
case 'common': return 'bg-black text-white border-slate-600';
@@ -22,15 +24,21 @@ const ListItem: React.FC<{ card: DraftCard }> = ({ card }) => {
return (
<li className="relative group">
<div className="flex items-center justify-between py-1 px-2 rounded hover:bg-slate-700/50 cursor-pointer">
<span className={`font-medium ${card.rarity === 'mythic' ? 'text-orange-400' : card.rarity === 'rare' ? 'text-yellow-400' : card.rarity === 'uncommon' ? 'text-slate-200' : 'text-slate-400'}`}>
<span className={`font-medium flex items-center gap-2 ${card.rarity === 'mythic' ? 'text-orange-400' : card.rarity === 'rare' ? 'text-yellow-400' : card.rarity === 'uncommon' ? 'text-slate-200' : 'text-slate-400'}`}>
{card.name}
{isFoil(card) && (
<span className="text-transparent bg-clip-text bg-gradient-to-r from-pink-300 via-purple-300 to-indigo-400 animate-pulse text-xs font-bold border border-purple-500/50 rounded px-1">
FOIL
</span>
)}
</span>
<span className={`w-2 h-2 rounded-full border ${getRarityColorClass(card.rarity)} !p-0 !text-[0px]`}></span>
</div>
{card.image && (
<div className="hidden group-hover:block absolute left-0 top-full z-50 mt-1 pointer-events-none">
<div className="bg-black p-1 rounded-lg border border-slate-500 shadow-2xl w-48">
<img src={card.image} alt={card.name} className="w-full rounded" />
<div className="bg-black p-1 rounded-lg border border-slate-500 shadow-2xl w-48 relative overflow-hidden">
{isFoil(card) && <div className="absolute inset-0 bg-gradient-to-br from-white/20 via-transparent to-white/10 opacity-50 z-10 pointer-events-none mix-blend-overlay animate-pulse" />}
<img src={card.image} alt={card.name} className="w-full rounded relative z-0" />
</div>
</div>
)}
@@ -43,6 +51,7 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode }) => {
const rares = pack.cards.filter(c => c.rarity === 'rare');
const uncommons = pack.cards.filter(c => c.rarity === 'uncommon');
const commons = pack.cards.filter(c => c.rarity === 'common');
const isFoil = (card: DraftCard) => card.finish === 'foil';
const copyPackToClipboard = () => {
const text = pack.cards.map(c => c.name).join('\n');
@@ -95,7 +104,10 @@ export const PackCard: React.FC<PackCardProps> = ({ pack, viewMode }) => {
{viewMode === 'grid' && (
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-3">
{pack.cards.map((card) => (
<div key={card.id} className="relative aspect-[2.5/3.5] bg-slate-900 rounded-lg overflow-hidden group hover:scale-105 transition-transform duration-200 shadow-xl border border-slate-800">
<div key={card.id} className={`relative aspect-[2.5/3.5] bg-slate-900 rounded-lg overflow-hidden group hover:scale-105 transition-transform duration-200 shadow-xl border ${isFoil(card) ? 'border-purple-400 shadow-purple-500/20' : 'border-slate-800'}`}>
{isFoil(card) && <div className="absolute inset-0 z-20 bg-gradient-to-tr from-purple-500/10 via-transparent to-pink-500/10 mix-blend-color-dodge pointer-events-none" />}
{isFoil(card) && <div className="absolute top-1 right-1 z-30 text-[10px] font-bold text-white bg-purple-600/80 px-1 rounded backdrop-blur-sm">FOIL</div>}
{card.image ? (
<img src={card.image} alt={card.name} className="w-full h-full object-cover" />
) : (

View File

@@ -129,7 +129,14 @@ export const CubeManager: React.FC<CubeManagerProps> = ({ packs, setPacks, onGoT
identifiers.forEach(id => {
const card = scryfallService.getCachedCard(id.type === 'id' ? { id: id.value } : { name: id.value });
if (card) {
for (let i = 0; i < id.quantity; i++) expandedCards.push(card);
for (let i = 0; i < id.quantity; i++) {
// Clone card to attach unique properties like finish
const expandedCard = { ...card };
if (id.finish) {
expandedCard.finish = id.finish;
}
expandedCards.push(expandedCard);
}
}
});
}

View File

@@ -2,6 +2,7 @@ export interface CardIdentifier {
type: 'id' | 'name';
value: string;
quantity: number;
finish?: 'foil' | 'normal';
}
export class CardParserService {
@@ -11,13 +12,29 @@ export class CardParserService {
const uuidRegex = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
lines.forEach(line => {
if (line.toLowerCase().startsWith('quantity') || line.toLowerCase().startsWith('count,name')) return;
// Skip header
if (line.toLowerCase().startsWith('quantity') && line.toLowerCase().includes('name')) return;
const idMatch = line.match(uuidRegex);
const cleanLineForQty = line.replace(/['"]/g, '');
const quantityMatch = cleanLineForQty.match(/^(\d+)[xX\s,;]/);
const quantity = quantityMatch ? parseInt(quantityMatch[1], 10) : 1;
// Detect Finish from CSV (Comma Separated)
let finish: 'foil' | 'normal' | undefined = undefined;
const parts = line.split(',');
if (parts.length >= 3) {
// Assuming format: Quantity,Name,Finish,...
// If the line started with a number, parts[0] is quantity. parts[1] is name. parts[2] is Finish.
// We should be careful about commas in names, but the user example shows a clean structure.
// If the name is quoted, split(',') might be naive, but valid for the provided example.
// Let's assume the user provided format: Quantity,Name,Finish,Edition Name,Scryfall ID
const possibleFinish = parts[2].trim().toLowerCase();
if (possibleFinish === 'foil' || possibleFinish === 'etched') finish = 'foil';
else if (possibleFinish === 'normal') finish = 'normal';
}
let identifier: { type: 'id' | 'name', value: string } | null = null;
if (idMatch) {
@@ -28,7 +45,6 @@ export class CardParserService {
let name = cleanLine.replace(/^(\d+)[xX\s,;]+/, '').trim();
// Remove set codes in parentheses/brackets e.g. (M20), [STA]
// This regex looks for ( starts, anything inside, ) ends, or same for []
name = name.replace(/\s*[\(\[].*?[\)\]]/g, '');
// Remove trailing collector numbers (digits at the very end)
@@ -44,16 +60,11 @@ export class CardParserService {
}
if (identifier) {
// Return one entry per quantity? Or aggregated?
// The original code pushed multiple entries to an array.
// For a parser service, returning the count is better, but to match logic:
// "for (let i = 0; i < quantity; i++) rawCardList.push(identifier);"
// I will return one object with Quantity property to be efficient.
rawCardList.push({
type: identifier.type,
value: identifier.value,
quantity: quantity
quantity: quantity,
finish: finish
});
}
});

View File

@@ -10,6 +10,7 @@ export interface DraftCard {
set: string;
setCode: string;
setType: string;
finish?: 'foil' | 'normal';
}
export interface Pack {
@@ -71,7 +72,8 @@ export class PackGeneratorService {
image: cardData.image_uris?.normal || cardData.card_faces?.[0]?.image_uris?.normal || '',
set: cardData.set_name,
setCode: cardData.set,
setType: setType
setType: setType,
finish: cardData.finish
};
// Add to pools

View File

@@ -10,6 +10,7 @@ export interface ScryfallCard {
colors?: string[];
image_uris?: { normal: string };
card_faces?: { image_uris: { normal: string } }[];
finish?: 'foil' | 'normal'; // Manual override from import
}
export class ScryfallService {