diff --git a/pages/api/cards/[id].js b/pages/api/cards/[id].js index 04cff2c..a53173e 100644 --- a/pages/api/cards/[id].js +++ b/pages/api/cards/[id].js @@ -1,116 +1,42 @@ import { sql } from '@vercel/postgres'; export default async function handler(req, res) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + const { id } = req.query; - if (req.method === 'GET') { - try { - const result = await sql` - SELECT - id, name, set_name, set_code, card_number, rarity, game, - mana_cost, cmc, card_type, colors, oracle_text, - power, toughness, image_url, stock_image_url, - current_price, market_price, scryfall_id, verified, - created_at, updated_at - FROM cards - WHERE id = ${id} - `; - - if (result.rows.length === 0) { - return res.status(404).json({ error: 'Card not found' }); - } - - const card = result.rows[0]; - - // Parse JSON fields - if (card.colors) { - try { - card.colors = JSON.parse(card.colors); - } catch (e) { - card.colors = []; - } - } - - res.status(200).json({ - success: true, - card - }); - - } catch (error) { - console.error('Card fetch error:', error); - res.status(500).json({ - error: 'Failed to fetch card', - details: error.message - }); - } - } else if (req.method === 'PUT') { - try { - const { - name, set_name, set_code, card_number, rarity, game, + try { + const result = await sql` + SELECT + id, name, set_name, set_code, card_number, rarity, game, mana_cost, cmc, card_type, colors, oracle_text, power, toughness, image_url, stock_image_url, - current_price, market_price - } = req.body; + current_price, market_price, scryfall_id, verified, + quantity + FROM cards + WHERE id = ${id} + `; - const result = await sql` - UPDATE cards SET - name = ${name}, - set_name = ${set_name}, - set_code = ${set_code}, - card_number = ${card_number}, - rarity = ${rarity}, - game = ${game}, - mana_cost = ${mana_cost}, - cmc = ${cmc}, - card_type = ${card_type}, - colors = ${JSON.stringify(colors)}, - oracle_text = ${oracle_text}, - power = ${power}, - toughness = ${toughness}, - image_url = ${image_url}, - stock_image_url = ${stock_image_url}, - current_price = ${current_price}, - market_price = ${market_price}, - updated_at = CURRENT_TIMESTAMP - WHERE id = ${id} - RETURNING * - `; + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Card not found' }); + } - if (result.rows.length === 0) { - return res.status(404).json({ error: 'Card not found' }); + const card = result.rows[0]; + + // Parse colors if it's a JSON string + if (card.colors && typeof card.colors === 'string') { + try { + card.colors = JSON.parse(card.colors); + } catch (e) { + card.colors = []; } - - res.status(200).json({ - success: true, - card: result.rows[0] - }); - - } catch (error) { - console.error('Card update error:', error); - res.status(500).json({ - error: 'Failed to update card', - details: error.message - }); } - } else if (req.method === 'DELETE') { - try { - const result = await sql` - DELETE FROM cards WHERE id = ${id} - `; - res.status(200).json({ - success: true, - message: 'Card deleted successfully' - }); - - } catch (error) { - console.error('Card delete error:', error); - res.status(500).json({ - error: 'Failed to delete card', - details: error.message - }); - } - } else { - res.status(405).json({ error: 'Method not allowed' }); + res.status(200).json(card); + } catch (error) { + console.error('Error fetching card:', error); + res.status(500).json({ error: 'Failed to fetch card' }); } } \ No newline at end of file diff --git a/pages/api/cards/[id]/collections.js b/pages/api/cards/[id]/collections.js new file mode 100644 index 0000000..047b07b --- /dev/null +++ b/pages/api/cards/[id]/collections.js @@ -0,0 +1,35 @@ +import { sql } from '@vercel/postgres'; + +export default async function handler(req, res) { + const { id } = req.query; + + if (req.method === 'GET') { + try { + // For now, return mock data until we implement the collections table + const mockCardCollections = [ + { id: 1, name: 'My MTG Collection' }, + { id: 4, name: 'Rare Cards' } + ]; + + res.status(200).json(mockCardCollections); + } catch (error) { + console.error('Error fetching card collections:', error); + res.status(500).json({ error: 'Failed to fetch card collections' }); + } + } else if (req.method === 'POST') { + try { + const { collectionId } = req.body; + + // For now, just return success until we implement the collections table + res.status(200).json({ + success: true, + message: 'Card added to collection' + }); + } catch (error) { + console.error('Error adding card to collection:', error); + res.status(500).json({ error: 'Failed to add card to collection' }); + } + } else { + res.status(405).json({ error: 'Method not allowed' }); + } +} \ No newline at end of file diff --git a/pages/api/cards/[id]/decks.js b/pages/api/cards/[id]/decks.js new file mode 100644 index 0000000..24c412a --- /dev/null +++ b/pages/api/cards/[id]/decks.js @@ -0,0 +1,35 @@ +import { sql } from '@vercel/postgres'; + +export default async function handler(req, res) { + const { id } = req.query; + + if (req.method === 'GET') { + try { + // For now, return mock data until we implement the decks table + const mockCardDecks = [ + { id: 1, name: 'MTG Control Deck' }, + { id: 4, name: 'MTG Combo' } + ]; + + res.status(200).json(mockCardDecks); + } catch (error) { + console.error('Error fetching card decks:', error); + res.status(500).json({ error: 'Failed to fetch card decks' }); + } + } else if (req.method === 'POST') { + try { + const { deckId } = req.body; + + // For now, just return success until we implement the decks table + res.status(200).json({ + success: true, + message: 'Card added to deck' + }); + } catch (error) { + console.error('Error adding card to deck:', error); + res.status(500).json({ error: 'Failed to add card to deck' }); + } + } else { + res.status(405).json({ error: 'Method not allowed' }); + } +} \ No newline at end of file diff --git a/pages/api/cards/[id]/favorite.js b/pages/api/cards/[id]/favorite.js new file mode 100644 index 0000000..ee7a2ce --- /dev/null +++ b/pages/api/cards/[id]/favorite.js @@ -0,0 +1,32 @@ +import { sql } from '@vercel/postgres'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const { id } = req.query; + const { favorited } = req.body; + + try { + // Update the card's favorite status + const result = await sql` + UPDATE cards + SET favorited = ${favorited} + WHERE id = ${id} + RETURNING id, name, favorited + `; + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Card not found' }); + } + + res.status(200).json({ + success: true, + card: result.rows[0] + }); + } catch (error) { + console.error('Error updating favorite status:', error); + res.status(500).json({ error: 'Failed to update favorite status' }); + } +} \ No newline at end of file diff --git a/pages/api/cards/[id]/ownership.js b/pages/api/cards/[id]/ownership.js new file mode 100644 index 0000000..b16634d --- /dev/null +++ b/pages/api/cards/[id]/ownership.js @@ -0,0 +1,32 @@ +import { sql } from '@vercel/postgres'; + +export default async function handler(req, res) { + if (req.method !== 'POST') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + const { id } = req.query; + const { quantity } = req.body; + + try { + // Update the card's quantity + const result = await sql` + UPDATE cards + SET quantity = ${quantity} + WHERE id = ${id} + RETURNING id, name, quantity + `; + + if (result.rows.length === 0) { + return res.status(404).json({ error: 'Card not found' }); + } + + res.status(200).json({ + success: true, + card: result.rows[0] + }); + } catch (error) { + console.error('Error updating ownership:', error); + res.status(500).json({ error: 'Failed to update ownership' }); + } +} \ No newline at end of file diff --git a/pages/api/collections.js b/pages/api/collections.js new file mode 100644 index 0000000..a179f29 --- /dev/null +++ b/pages/api/collections.js @@ -0,0 +1,23 @@ +import { sql } from '@vercel/postgres'; + +export default async function handler(req, res) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + // For now, return mock collections until we implement user authentication + const mockCollections = [ + { id: 1, name: 'My MTG Collection', game: 'MTG' }, + { id: 2, name: 'Pokemon Favorites', game: 'Pokemon' }, + { id: 3, name: 'Lorcana Disney', game: 'Lorcana' }, + { id: 4, name: 'Rare Cards', game: 'MTG' }, + { id: 5, name: 'Holographic Collection', game: 'Pokemon' } + ]; + + res.status(200).json(mockCollections); + } catch (error) { + console.error('Error fetching collections:', error); + res.status(500).json({ error: 'Failed to fetch collections' }); + } +} \ No newline at end of file diff --git a/pages/api/decks.js b/pages/api/decks.js new file mode 100644 index 0000000..3c82090 --- /dev/null +++ b/pages/api/decks.js @@ -0,0 +1,23 @@ +import { sql } from '@vercel/postgres'; + +export default async function handler(req, res) { + if (req.method !== 'GET') { + return res.status(405).json({ error: 'Method not allowed' }); + } + + try { + // For now, return mock decks until we implement user authentication + const mockDecks = [ + { id: 1, name: 'MTG Control Deck', game: 'MTG' }, + { id: 2, name: 'Pokemon Aggro', game: 'Pokemon' }, + { id: 3, name: 'Lorcana Midrange', game: 'Lorcana' }, + { id: 4, name: 'MTG Combo', game: 'MTG' }, + { id: 5, name: 'Pokemon Stall', game: 'Pokemon' } + ]; + + res.status(200).json(mockDecks); + } catch (error) { + console.error('Error fetching decks:', error); + res.status(500).json({ error: 'Failed to fetch decks' }); + } +} \ No newline at end of file diff --git a/pages/card/[id].js b/pages/card/[id].js index 423aaa8..0e136f7 100644 --- a/pages/card/[id].js +++ b/pages/card/[id].js @@ -22,152 +22,81 @@ export default function CardDetail() { const [selectedDeck, setSelectedDeck] = useState(''); const [quantity, setQuantity] = useState(1); const [isFavorited, setIsFavorited] = useState(false); + const [collections, setCollections] = useState([]); + const [decks, setDecks] = useState([]); + const [cardCollections, setCardCollections] = useState([]); + const [cardDecks, setCardDecks] = useState([]); - // Mock card data - in real app this would come from API - const mockCards = { - '1': { - id: 1, - name: "Black Lotus", - game: "MTG", - rarity: "mythic", - rarityColor: "#FFD700", - rarityGradient: "from-yellow-400 to-orange-500", - set: "Alpha", - value: 25000, - image: "/api/placeholder/1", - description: "The most iconic Magic card ever printed", - type: "Artifact", - condition: "Near Mint", - artist: "Christopher Rush", - cardNumber: "232", - flavorText: "The most powerful artifact ever created.", - manaCost: "{0}", - power: null, - toughness: null, - text: "{T}, Sacrifice Black Lotus: Add three mana of any one color to your mana pool.", - rulings: [ - "Black Lotus is banned in all formats except Vintage.", - "The card was printed in Alpha, Beta, and Unlimited editions.", - "It's considered one of the Power Nine." - ], - priceHistory: [ - { date: '2023-01-01', price: 22000 }, - { date: '2023-04-01', price: 23500 }, - { date: '2023-07-01', price: 24000 }, - { date: '2023-10-01', price: 24500 }, - { date: '2024-01-01', price: 25000 } - ], - purchaseLinks: [ - { name: 'TCGPlayer', url: 'https://www.tcgplayer.com/search/magic/product?productName=black+lotus', icon: '🃏' }, - { name: 'eBay', url: 'https://www.ebay.com/sch/i.html?_nkw=black+lotus+magic+alpha', icon: '🛒' }, - { name: 'Card Kingdom', url: 'https://www.cardkingdom.com/catalog/search?search=black+lotus', icon: '👑' }, - { name: 'Star City Games', url: 'https://starcitygames.com/search?searchQuery=black+lotus', icon: '⭐' } - ] - }, - '2': { - id: 2, - name: "Charizard", - game: "Pokemon", - rarity: "holographic", - rarityColor: "#FF6B6B", - rarityGradient: "from-red-400 to-pink-500", - set: "Base Set", - value: 350, - image: "/api/placeholder/2", - description: "The classic holographic Charizard", - type: "Fire", - condition: "Lightly Played", - artist: "Mitsuhiro Arita", - cardNumber: "4/102", - flavorText: "Spits fire that is hot enough to melt boulders.", - manaCost: null, - power: "100", - toughness: null, - text: "Fire Spin: 100 damage. Discard 2 Energy cards attached to Charizard in order to use this attack.", - rulings: [ - "This is the most valuable card from the original Base Set.", - "The holographic version is significantly more valuable than the non-holo.", - "First printed in 1999." - ], - priceHistory: [ - { date: '2023-01-01', price: 300 }, - { date: '2023-04-01', price: 320 }, - { date: '2023-07-01', price: 330 }, - { date: '2023-10-01', price: 340 }, - { date: '2024-01-01', price: 350 } - ], - purchaseLinks: [ - { name: 'TCGPlayer', url: 'https://www.tcgplayer.com/search/pokemon/product?productName=charizard+base+set', icon: '🃏' }, - { name: 'eBay', url: 'https://www.ebay.com/sch/i.html?_nkw=charizard+base+set+holographic', icon: '🛒' }, - { name: 'Pokemon Center', url: 'https://www.pokemoncenter.com/search?q=charizard', icon: '⚡' } - ] - }, - '3': { - id: 3, - name: "Mickey Mouse", - game: "Lorcana", - rarity: "enchanted", - rarityColor: "#A855F7", - rarityGradient: "from-purple-400 to-indigo-500", - set: "First Chapter", - value: 45, - image: "/api/placeholder/3", - description: "Disney's iconic character in card form", - type: "Character", - condition: "Near Mint", - artist: "Disney", - cardNumber: "1/204", - flavorText: "The most magical mouse of all.", - manaCost: "2", - power: "2", - toughness: "2", - text: "When this character enters play, you may draw a card.", - rulings: [ - "This is the first Disney card game featuring Mickey Mouse.", - "The enchanted version has special foil treatment.", - "Released in 2023." - ], - priceHistory: [ - { date: '2023-08-01', price: 40 }, - { date: '2023-10-01', price: 42 }, - { date: '2023-12-01', price: 44 }, - { date: '2024-01-01', price: 45 } - ], - purchaseLinks: [ - { name: 'TCGPlayer', url: 'https://www.tcgplayer.com/search/lorcana/product?productName=mickey+mouse', icon: '🃏' }, - { name: 'eBay', url: 'https://www.ebay.com/sch/i.html?_nkw=mickey+mouse+lorcana+enchanted', icon: '🛒' }, - { name: 'Disney Store', url: 'https://www.shopdisney.com/search?searchQuery=lorcana', icon: '✨' } - ] - } - }; - - // Mock collections and decks data - const mockCollections = [ - { id: 1, name: 'My MTG Collection', game: 'MTG' }, - { id: 2, name: 'Pokemon Favorites', game: 'Pokemon' }, - { id: 3, name: 'Lorcana Disney', game: 'Lorcana' }, - { id: 4, name: 'Rare Cards', game: 'MTG' }, - { id: 5, name: 'Holographic Collection', game: 'Pokemon' } - ]; - - const mockDecks = [ - { id: 1, name: 'MTG Control Deck', game: 'MTG' }, - { id: 2, name: 'Pokemon Aggro', game: 'Pokemon' }, - { id: 3, name: 'Lorcana Midrange', game: 'Lorcana' }, - { id: 4, name: 'MTG Combo', game: 'MTG' }, - { id: 5, name: 'Pokemon Stall', game: 'Pokemon' } - ]; - + // Fetch card data from API useEffect(() => { - if (id && mockCards[id]) { - setCard(mockCards[id]); - setLoading(false); - } else if (id) { - // Card not found - setLoading(false); - } + const fetchCard = async () => { + if (!id) return; + + try { + const response = await fetch(`/api/cards/${id}`); + if (response.ok) { + const cardData = await response.json(); + setCard(cardData); + + // Set initial owned quantity if available + if (cardData.quantity) { + setOwnedQuantity(cardData.quantity); + } + } else { + console.error('Failed to fetch card'); + } + } catch (error) { + console.error('Error fetching card:', error); + } finally { + setLoading(false); + } + }; + + fetchCard(); }, [id]); + // Fetch user's collections and decks + useEffect(() => { + const fetchUserData = async () => { + try { + // Fetch collections + const collectionsResponse = await fetch('/api/collections'); + if (collectionsResponse.ok) { + const collectionsData = await collectionsResponse.json(); + setCollections(collectionsData); + } + + // Fetch decks + const decksResponse = await fetch('/api/decks'); + if (decksResponse.ok) { + const decksData = await decksResponse.json(); + setDecks(decksData); + } + + // Fetch card's current collections and decks + if (card) { + const cardCollectionsResponse = await fetch(`/api/cards/${id}/collections`); + if (cardCollectionsResponse.ok) { + const cardCollectionsData = await cardCollectionsResponse.json(); + setCardCollections(cardCollectionsData); + } + + const cardDecksResponse = await fetch(`/api/cards/${id}/decks`); + if (cardDecksResponse.ok) { + const cardDecksData = await cardDecksResponse.json(); + setCardDecks(cardDecksData); + } + } + } catch (error) { + console.error('Error fetching user data:', error); + } + }; + + if (card) { + fetchUserData(); + } + }, [card, id]); + const getTCGGradient = (game) => { const gradients = { 'MTG': 'from-purple-600 via-purple-500 to-indigo-600', @@ -187,6 +116,7 @@ export default function CardDetail() { }; const formatCurrency = (amount) => { + if (!amount) return '$0.00'; return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' @@ -200,9 +130,120 @@ export default function CardDetail() { 'rare': 'Rare', 'mythic': 'Mythic', 'holographic': 'Holographic', - 'enchanted': 'Enchanted' + 'enchanted': 'Enchanted', + 'super rare': 'Super Rare', + 'legendary': 'Legendary' }; - return rarityMap[rarity] || rarity; + return rarityMap[rarity?.toLowerCase()] || rarity; + }; + + const getRarityColor = (rarity) => { + const colors = { + 'common': '#6B7280', + 'uncommon': '#10B981', + 'rare': '#F59E0B', + 'mythic': '#FFD700', + 'holographic': '#FF6B6B', + 'enchanted': '#A855F7', + 'super rare': '#3B82F6', + 'legendary': '#FFD700' + }; + return colors[rarity?.toLowerCase()] || '#6B7280'; + }; + + const handleOwnershipUpdate = async (newQuantity) => { + try { + const response = await fetch(`/api/cards/${id}/ownership`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ quantity: newQuantity }) + }); + + if (response.ok) { + setOwnedQuantity(newQuantity); + setShowQuantityModal(false); + } else { + console.error('Failed to update ownership'); + } + } catch (error) { + console.error('Error updating ownership:', error); + } + }; + + const handleAddToCollection = async () => { + try { + const response = await fetch(`/api/cards/${id}/collections`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ collectionId: selectedCollection }) + }); + + if (response.ok) { + // Refresh card collections + const cardCollectionsResponse = await fetch(`/api/cards/${id}/collections`); + if (cardCollectionsResponse.ok) { + const cardCollectionsData = await cardCollectionsResponse.json(); + setCardCollections(cardCollectionsData); + } + setShowCollectionModal(false); + setSelectedCollection(''); + } else { + console.error('Failed to add to collection'); + } + } catch (error) { + console.error('Error adding to collection:', error); + } + }; + + const handleAddToDeck = async () => { + try { + const response = await fetch(`/api/cards/${id}/decks`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ deckId: selectedDeck }) + }); + + if (response.ok) { + // Refresh card decks + const cardDecksResponse = await fetch(`/api/cards/${id}/decks`); + if (cardDecksResponse.ok) { + const cardDecksData = await cardDecksResponse.json(); + setCardDecks(cardDecksData); + } + setShowDeckModal(false); + setSelectedDeck(''); + } else { + console.error('Failed to add to deck'); + } + } catch (error) { + console.error('Error adding to deck:', error); + } + }; + + const handleToggleFavorite = async () => { + try { + const response = await fetch(`/api/cards/${id}/favorite`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ favorited: !isFavorited }) + }); + + if (response.ok) { + setIsFavorited(!isFavorited); + } else { + console.error('Failed to toggle favorite'); + } + } catch (error) { + console.error('Error toggling favorite:', error); + } }; if (loading) { @@ -263,22 +304,30 @@ export default function CardDetail() {
-
-
-
{getTCGIcon(card.game)}
-

{card.name}

-

{card.set}

-
- - {getRarityLabel(card.rarity)} - + {card.image_url ? ( + {card.name} + ) : ( +
+
+
{getTCGIcon(card.game)}
+

{card.name}

+

{card.set_name}

+
+ + {getRarityLabel(card.rarity)} + +
-
+ )}
@@ -287,7 +336,7 @@ export default function CardDetail() {

{card.name}

-

{card.description}

+

{card.oracle_text || card.card_type}

{/* Ownership Status and Actions */}
@@ -300,7 +349,7 @@ export default function CardDetail() { )}
- {formatCurrency(card.value)} + {formatCurrency(card.current_price || 0)}
- {/* Quick Actions */} -
- {card.purchaseLinks.slice(0, 3).map((link, index) => ( - - {link.icon} - {link.name} - - ))} -
+ {/* Current Collections and Decks */} + {(cardCollections.length > 0 || cardDecks.length > 0) && ( +
+

Currently In:

+
+ {cardCollections.map(collection => ( +
+ 📁 + {collection.name} +
+ ))} + {cardDecks.map(deck => ( +
+ 🎴 + {deck.name} +
+ ))} +
+
+ )}
@@ -408,32 +462,24 @@ export default function CardDetail() {
Set - {card.set} + {card.set_name}
Card Number - {card.cardNumber} + {card.card_number}
Type - {card.type} + {card.card_type}
Rarity {getRarityLabel(card.rarity)}
-
- Artist - {card.artist} -
-
- Condition - {card.condition} -
- {card.manaCost && ( + {card.mana_cost && (
Cost to Play - {card.manaCost} + {card.mana_cost}
)} {card.power && ( @@ -448,30 +494,6 @@ export default function CardDetail() { {card.toughness} )} - {card.hp && ( -
- HP - {card.hp} -
- )} - {card.form && ( -
- Form - {card.form} -
- )} - {card.weakness && ( -
- Weakness - {card.weakness} -
- )} - {card.retreat_cost && ( -
- Retreat Cost - {card.retreat_cost} -
- )} {card.current_price && (
Current Price @@ -481,41 +503,18 @@ export default function CardDetail() {
- {/* Card Text and Rulings */} + {/* Card Text */}

Card Text

- {card.flavorText && ( + {card.oracle_text && (
-

- "{card.flavorText}" -

-
- )} - {card.text && ( -
-

{card.text}

+

{card.oracle_text}

)}
- - {card.rulings && card.rulings.length > 0 && ( -
-

- Rulings -

- -
- )}
)} @@ -526,58 +525,15 @@ export default function CardDetail() { Price History - {/* Price Graph */} + {/* Price Graph Placeholder */}

Price Trend

-
- {/* Graph Container */} -
- {card.priceHistory.map((entry, index) => { - const maxPrice = Math.max(...card.priceHistory.map(p => p.price)); - const minPrice = Math.min(...card.priceHistory.map(p => p.price)); - const priceRange = maxPrice - minPrice; - const height = priceRange > 0 ? ((entry.price - minPrice) / priceRange) * 100 : 50; - - return ( -
- {/* Bar */} -
- {/* Price Label */} -
-
- {formatCurrency(entry.price)} -
-
- {new Date(entry.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })} -
-
-
- ); - })} -
- - {/* Grid Lines */} -
- {[0, 25, 50, 75, 100].map((line) => ( -
- ))} -
+
+

+ Price history data will be available soon +

@@ -595,91 +551,10 @@ export default function CardDetail() { Current Price
- {formatCurrency(card.value)} + {formatCurrency(card.current_price || 0)}
- - {/* Lowest Price */} -
-
-
📉
-
- Lowest Price -
-
- {formatCurrency(Math.min(...card.priceHistory.map(p => p.price)))} -
-
-
- - {/* Highest Price */} -
-
-
📈
-
- Highest Price -
-
- {formatCurrency(Math.max(...card.priceHistory.map(p => p.price)))} -
-
-
- - {/* Average Price */} -
-
-
📊
-
- Average Price -
-
- {formatCurrency(card.priceHistory.reduce((sum, p) => sum + p.price, 0) / card.priceHistory.length)} -
-
-
- - - {/* Price Change Indicator */} -
-
- Price Change -
- {(() => { - const firstPrice = card.priceHistory[0].price; - const lastPrice = card.priceHistory[card.priceHistory.length - 1].price; - const change = lastPrice - firstPrice; - const changePercent = ((change / firstPrice) * 100).toFixed(1); - const isPositive = change >= 0; - - return ( - <> - - {isPositive ? '+' : ''}{formatCurrency(change)} - - - ({isPositive ? '+' : ''}{changePercent}%) - - - {isPositive ? '📈' : '📉'} - - - ); - })()} -
-
)} @@ -690,31 +565,50 @@ export default function CardDetail() { Where to Buy
- {card.purchaseLinks.map((link, index) => ( - -
- {link.icon} -
-

- {link.name} -

-

- View on {link.name} -

-
+
+
+ 🃏 +
+

+ TCGPlayer +

+

+ View on TCGPlayer +

-
- ))} +
+ + +
+ 🛒 +
+

+ eBay +

+

+ View on eBay +

+
+
+
)} @@ -762,10 +656,7 @@ export default function CardDetail() {