255 lines
6.8 KiB
JavaScript
255 lines
6.8 KiB
JavaScript
|
|
import { useState, useEffect, useRef } from 'react';
|
||
|
|
import { useRouter } from 'next/router';
|
||
|
|
import { isBasicLand } from './deck-builder-stats.js';
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Deck builder page state and handlers (god-component split).
|
||
|
|
*/
|
||
|
|
export function useDeckBuilder({ user = null, authLoading = true } = {}) {
|
||
|
|
const router = useRouter();
|
||
|
|
const { deck: deckId } = router.query;
|
||
|
|
|
||
|
|
const [deck, setDeck] = useState(null);
|
||
|
|
const [deckCards, setDeckCards] = useState([]);
|
||
|
|
const [searchResults, setSearchResults] = useState([]);
|
||
|
|
const [searchQuery, setSearchQuery] = useState('');
|
||
|
|
const [loading, setLoading] = useState(true);
|
||
|
|
const [searchLoading, setSearchLoading] = useState(false);
|
||
|
|
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||
|
|
const [selectedCard, setSelectedCard] = useState(null);
|
||
|
|
const [showFilters, setShowFilters] = useState(false);
|
||
|
|
const [showSettings, setShowSettings] = useState(false);
|
||
|
|
const [viewMode, setViewMode] = useState('list');
|
||
|
|
const [manaSymbolSettings, setManaSymbolSettings] = useState({ useSVG: false });
|
||
|
|
const [filters, setFilters] = useState({
|
||
|
|
colors: [],
|
||
|
|
types: [],
|
||
|
|
cmc: '',
|
||
|
|
rarity: '',
|
||
|
|
});
|
||
|
|
|
||
|
|
const searchTimeoutRef = useRef(null);
|
||
|
|
|
||
|
|
const fetchDeck = async () => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
const response = await fetch(`/api/decks/${deckId}`, {
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${token}`,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const data = await response.json();
|
||
|
|
setDeck(data);
|
||
|
|
setDeckCards(data.cards || []);
|
||
|
|
} else {
|
||
|
|
console.error('Failed to fetch deck');
|
||
|
|
router.push('/decks');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error fetching deck:', error);
|
||
|
|
router.push('/decks');
|
||
|
|
} finally {
|
||
|
|
setLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const searchCards = async () => {
|
||
|
|
setSearchLoading(true);
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
const params = new URLSearchParams({
|
||
|
|
game: 'MTG',
|
||
|
|
limit: '50',
|
||
|
|
});
|
||
|
|
|
||
|
|
if (searchQuery.trim()) {
|
||
|
|
params.append('search', searchQuery);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (filters.colors.length > 0) {
|
||
|
|
params.append('colors', filters.colors.join(','));
|
||
|
|
}
|
||
|
|
if (filters.types.length > 0) {
|
||
|
|
params.append('types', filters.types.join(','));
|
||
|
|
}
|
||
|
|
if (filters.cmc) {
|
||
|
|
params.append('cmc', filters.cmc);
|
||
|
|
}
|
||
|
|
if (filters.rarity) {
|
||
|
|
params.append('rarity', filters.rarity);
|
||
|
|
}
|
||
|
|
|
||
|
|
const response = await fetch(`/api/cards/search?${params}`, {
|
||
|
|
headers: {
|
||
|
|
Authorization: `Bearer ${token}`,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
const data = await response.json();
|
||
|
|
setSearchResults(data.cards || []);
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error searching cards:', error);
|
||
|
|
} finally {
|
||
|
|
setSearchLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (user && deckId) {
|
||
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- load deck when route id or user changes
|
||
|
|
fetchDeck();
|
||
|
|
}
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- load deck when route id or user changes
|
||
|
|
}, [user, deckId]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (user) {
|
||
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- initial catalog load once user is known
|
||
|
|
searchCards();
|
||
|
|
}
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- initial catalog load once user is known
|
||
|
|
}, [user]);
|
||
|
|
|
||
|
|
useEffect(() => {
|
||
|
|
if (searchTimeoutRef.current) {
|
||
|
|
clearTimeout(searchTimeoutRef.current);
|
||
|
|
}
|
||
|
|
searchTimeoutRef.current = setTimeout(() => {
|
||
|
|
searchCards();
|
||
|
|
}, 300);
|
||
|
|
|
||
|
|
return () => {
|
||
|
|
if (searchTimeoutRef.current) {
|
||
|
|
clearTimeout(searchTimeoutRef.current);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- debounced search; searchCards reads latest filter state
|
||
|
|
}, [searchQuery, filters]);
|
||
|
|
|
||
|
|
const addCardToDeck = async (card, quantity = 1) => {
|
||
|
|
if (deck?.format === 'Commander') {
|
||
|
|
const existingCard = deckCards.find((dc) => dc.card_id === card.id);
|
||
|
|
const currentQuantity = existingCard ? existingCard.quantity : 0;
|
||
|
|
|
||
|
|
if (!isBasicLand(card) && currentQuantity + quantity > 1) {
|
||
|
|
alert('Commander format allows only 1 copy of each non-basic land card.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const totalCards = deckCards.reduce((sum, dc) => sum + dc.quantity, 0);
|
||
|
|
if (totalCards + quantity > 100) {
|
||
|
|
alert('Commander decks can have a maximum of 100 cards.');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
const response = await fetch(`/api/decks/${deckId}/cards`, {
|
||
|
|
method: 'POST',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
Authorization: `Bearer ${token}`,
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
cardId: card.id,
|
||
|
|
quantity,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
fetchDeck();
|
||
|
|
} else {
|
||
|
|
console.error('Failed to add card to deck');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error adding card to deck:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const removeCardFromDeck = async (cardId, quantity = 1) => {
|
||
|
|
try {
|
||
|
|
const token = localStorage.getItem('auth_token');
|
||
|
|
const response = await fetch(`/api/decks/${deckId}/cards`, {
|
||
|
|
method: 'DELETE',
|
||
|
|
headers: {
|
||
|
|
'Content-Type': 'application/json',
|
||
|
|
Authorization: `Bearer ${token}`,
|
||
|
|
},
|
||
|
|
body: JSON.stringify({
|
||
|
|
cardId,
|
||
|
|
quantity,
|
||
|
|
}),
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.ok) {
|
||
|
|
fetchDeck();
|
||
|
|
} else {
|
||
|
|
console.error('Failed to remove card from deck');
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
console.error('Error removing card from deck:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const toggleColorFilter = (color) => {
|
||
|
|
setFilters((prev) => ({
|
||
|
|
...prev,
|
||
|
|
colors: prev.colors.includes(color)
|
||
|
|
? prev.colors.filter((c) => c !== color)
|
||
|
|
: [...prev.colors, color],
|
||
|
|
}));
|
||
|
|
};
|
||
|
|
|
||
|
|
const clearFilters = () => {
|
||
|
|
setFilters({
|
||
|
|
colors: [],
|
||
|
|
types: [],
|
||
|
|
cmc: '',
|
||
|
|
rarity: '',
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
const showLoggedOut = !authLoading && !user;
|
||
|
|
const showDeckLoading = Boolean(user) && loading;
|
||
|
|
const showDeckNotFound = Boolean(user) && !loading && !deck;
|
||
|
|
|
||
|
|
return {
|
||
|
|
addCardToDeck,
|
||
|
|
authLoading,
|
||
|
|
clearFilters,
|
||
|
|
deck,
|
||
|
|
deckCards,
|
||
|
|
deckId,
|
||
|
|
filters,
|
||
|
|
loading,
|
||
|
|
manaSymbolSettings,
|
||
|
|
removeCardFromDeck,
|
||
|
|
searchLoading,
|
||
|
|
searchQuery,
|
||
|
|
searchResults,
|
||
|
|
selectedCard,
|
||
|
|
setFilters,
|
||
|
|
setManaSymbolSettings,
|
||
|
|
setSearchQuery,
|
||
|
|
setSelectedCard,
|
||
|
|
setShowFilters,
|
||
|
|
setShowSettings,
|
||
|
|
setSidebarOpen,
|
||
|
|
setViewMode,
|
||
|
|
showDeckLoading,
|
||
|
|
showDeckNotFound,
|
||
|
|
showFilters,
|
||
|
|
showLoggedOut,
|
||
|
|
showSettings,
|
||
|
|
sidebarOpen,
|
||
|
|
toggleColorFilter,
|
||
|
|
user,
|
||
|
|
viewMode,
|
||
|
|
};
|
||
|
|
}
|