feat: Add support for card finishes (foil/normal) from input parsing through to UI display.
This commit is contained in:
@@ -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" />
|
||||
) : (
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user