Completes card detail god-component split: thin page composer with loading/not-found branches. Co-authored-by: Cursor <cursoragent@cursor.com>
400 lines
12 KiB
JavaScript
400 lines
12 KiB
JavaScript
import { useState, useEffect, useMemo } from 'react';
|
|
import { useRouter } from 'next/router';
|
|
import { VOCAB, collectionDisplayName } from './collection-vocabulary.js';
|
|
|
|
/**
|
|
* Card/collection page state and handlers (god-component split).
|
|
*/
|
|
export function useCardDetail({ user, authLoading }) {
|
|
const router = useRouter();
|
|
const isAdmin = user?.role === 'admin';
|
|
const adminLoading = authLoading;
|
|
const { id } = router.query;
|
|
|
|
const [card, setCard] = useState(null);
|
|
const [loading, setLoading] = useState(true);
|
|
const [activeTab, setActiveTab] = useState('details');
|
|
const [ownedQuantity, setOwnedQuantity] = useState(0);
|
|
const [showQuantityModal, setShowQuantityModal] = useState(false);
|
|
const [showCollectionModal, setShowCollectionModal] = useState(false);
|
|
const [showDeckModal, setShowDeckModal] = useState(false);
|
|
const [selectedCollection, setSelectedCollection] = useState('');
|
|
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([]);
|
|
|
|
// Mana symbol settings
|
|
const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false });
|
|
|
|
// Fetch card data from API
|
|
useEffect(() => {
|
|
const fetchCard = async () => {
|
|
if (!id) return;
|
|
|
|
try {
|
|
const response = await fetch(`/api/cards/${id}`);
|
|
if (response.ok) {
|
|
const cardData = await response.json();
|
|
setCard(cardData);
|
|
|
|
// Fetch user's ownership of this card
|
|
const token = localStorage.getItem('auth_token');
|
|
if (token) {
|
|
try {
|
|
const ownershipResponse = await fetch(`/api/cards/${id}/ownership`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (ownershipResponse.ok) {
|
|
const ownershipData = await ownershipResponse.json();
|
|
setOwnedQuantity(ownershipData.quantity || 0);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching ownership:', error);
|
|
}
|
|
|
|
// Check if card is favorited
|
|
try {
|
|
const favoritesResponse = await fetch(`/api/favorites?type=card`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
if (favoritesResponse.ok) {
|
|
const favoritesData = await favoritesResponse.json();
|
|
const isCardFavorited = favoritesData.favorites.some(fav => fav.item_id == id);
|
|
setIsFavorited(isCardFavorited);
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking favorites:', error);
|
|
}
|
|
}
|
|
} 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 {
|
|
const token = localStorage.getItem('auth_token');
|
|
const headers = {
|
|
'Authorization': `Bearer ${token}`
|
|
};
|
|
|
|
// Fetch collections
|
|
const collectionsResponse = await fetch('/api/collections', { headers });
|
|
if (collectionsResponse.ok) {
|
|
const collectionsData = await collectionsResponse.json();
|
|
setCollections(collectionsData);
|
|
}
|
|
|
|
// Fetch decks
|
|
const decksResponse = await fetch('/api/decks', { headers });
|
|
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`, { headers });
|
|
if (cardCollectionsResponse.ok) {
|
|
const cardCollectionsData = await cardCollectionsResponse.json();
|
|
setCardCollections(cardCollectionsData);
|
|
}
|
|
|
|
const cardDecksResponse = await fetch(`/api/cards/${id}/decks`, { headers });
|
|
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 getRarityGradient = (rarity) => {
|
|
const rarityKey = rarity?.toLowerCase();
|
|
const gradients = {
|
|
'common': '#9ca3af, #6b7280, #4b5563', // Subtle gray glow
|
|
'uncommon': '#10b981, #059669, #047857', // Green glow
|
|
'rare': '#f59e0b, #d97706, #b45309', // Gold glow
|
|
'mythic': '#fbbf24, #f59e0b, #d97706', // Rich gold glow
|
|
'holographic': '#ec4899, #db2777, #be185d', // Pink glow
|
|
'enchanted': '#a855f7, #9333ea, #7c3aed', // Purple glow
|
|
'super rare': '#3b82f6, #2563eb, #1d4ed8', // Blue glow
|
|
'legendary': '#fbbf24, #f59e0b, #ea580c' // Vibrant gold-orange glow
|
|
};
|
|
return gradients[rarityKey] || '#6b7280, #4b5563, #374151';
|
|
};
|
|
|
|
const getParticleCount = (rarity) => {
|
|
const rarityKey = rarity?.toLowerCase();
|
|
const particleCounts = {
|
|
'common': 0,
|
|
'uncommon': 15,
|
|
'rare': 25,
|
|
'mythic': 40,
|
|
'holographic': 50,
|
|
'enchanted': 60,
|
|
'super rare': 45,
|
|
'legendary': 80
|
|
};
|
|
return particleCounts[rarityKey] || 0;
|
|
};
|
|
|
|
const particleStyles = useMemo(() => {
|
|
if (!card) return [];
|
|
const count = getParticleCount(card.rarity);
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
left: `${((i * 37) % 100)}%`,
|
|
top: `${((i * 53) % 100)}%`,
|
|
animationDelay: `${i % 3}s`,
|
|
animationDuration: `${3 + (i % 2)}s`,
|
|
}));
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- stable decorative layout per card id/rarity
|
|
}, [card?.id, card?.rarity]);
|
|
|
|
const getParticleColor = (rarity) => {
|
|
const rarityKey = rarity?.toLowerCase();
|
|
const colors = {
|
|
'common': '#ffffff',
|
|
'uncommon': '#10b981',
|
|
'rare': '#f59e0b',
|
|
'mythic': '#ffd700',
|
|
'holographic': '#ff6b6b',
|
|
'enchanted': '#a855f7',
|
|
'super rare': '#3b82f6',
|
|
'legendary': '#ffd700'
|
|
};
|
|
return colors[rarityKey] || '#ffffff';
|
|
};
|
|
|
|
const getTCGIcon = (game) => {
|
|
const icons = {
|
|
'MTG': '🔮',
|
|
'Pokemon': '⚡',
|
|
'Lorcana': '✨'
|
|
};
|
|
return icons[game] || '🃏';
|
|
};
|
|
|
|
const formatCurrency = (amount) => {
|
|
if (!amount) return '$0.00';
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD'
|
|
}).format(amount);
|
|
};
|
|
|
|
const getRarityLabel = (rarity) => {
|
|
const rarityMap = {
|
|
'common': 'Common',
|
|
'uncommon': 'Uncommon',
|
|
'rare': 'Rare',
|
|
'mythic': 'Mythic',
|
|
'holographic': 'Holographic',
|
|
'enchanted': 'Enchanted',
|
|
'super rare': 'Super Rare',
|
|
'legendary': 'Legendary'
|
|
};
|
|
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 token = localStorage.getItem('auth_token');
|
|
const response = await fetch(`/api/cards/${id}/ownership`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
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 token = localStorage.getItem('auth_token');
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
};
|
|
|
|
const response = await fetch(`/api/cards/${id}/collections`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({ collectionId: selectedCollection })
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Refresh card collections
|
|
const cardCollectionsResponse = await fetch(`/api/cards/${id}/collections`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
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 token = localStorage.getItem('auth_token');
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
};
|
|
|
|
const response = await fetch(`/api/cards/${id}/decks`, {
|
|
method: 'POST',
|
|
headers,
|
|
body: JSON.stringify({ deckId: selectedDeck })
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Refresh card decks
|
|
const cardDecksResponse = await fetch(`/api/cards/${id}/decks`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
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 token = localStorage.getItem('auth_token');
|
|
const response = await fetch(`/api/cards/${id}/favorite`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'Authorization': `Bearer ${token}`
|
|
},
|
|
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);
|
|
}
|
|
};
|
|
|
|
const showInitialLoading = authLoading || loading;
|
|
|
|
return {
|
|
activeTab,
|
|
card,
|
|
cardCollections,
|
|
cardDecks,
|
|
collections,
|
|
decks,
|
|
formatCurrency,
|
|
getParticleColor,
|
|
getParticleCount,
|
|
getRarityColor,
|
|
getRarityGradient,
|
|
getRarityLabel,
|
|
getTCGIcon,
|
|
handleAddToCollection,
|
|
handleAddToDeck,
|
|
handleOwnershipUpdate,
|
|
handleToggleFavorite,
|
|
id,
|
|
isAdmin,
|
|
adminLoading,
|
|
isFavorited,
|
|
loading,
|
|
manaSymbolSettings,
|
|
ownedQuantity,
|
|
particleStyles,
|
|
getParticleCount,
|
|
quantity,
|
|
router,
|
|
selectedCollection,
|
|
selectedDeck,
|
|
setActiveTab,
|
|
setCard,
|
|
setCardCollections,
|
|
setCardDecks,
|
|
setCollections,
|
|
setDecks,
|
|
setIsFavorited,
|
|
setLoading,
|
|
setManaSymbolSettings,
|
|
setOwnedQuantity,
|
|
setQuantity,
|
|
setSelectedCollection,
|
|
setSelectedDeck,
|
|
setShowCollectionModal,
|
|
setShowDeckModal,
|
|
setShowQuantityModal,
|
|
showCollectionModal,
|
|
showDeckModal,
|
|
showInitialLoading,
|
|
showQuantityModal
|
|
};
|
|
}
|