deckhearth/lib/use-collection-view.js

502 lines
14 KiB
JavaScript
Raw Permalink Normal View History

import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { VOCAB, collectionDisplayName } from './collection-vocabulary.js';
import { downloadCollectionCardsCsv } from './collection-cards-csv.js';
/**
* Card/collection page state and handlers (god-component split).
*/
export function useCollectionView({ user = null, authLoading = true } = {}) {
const router = useRouter();
const { identifier } = router.query;
const [collection, setCollection] = useState(null);
const [cards, setCards] = useState([]);
const [loading, setLoading] = useState(true);
const [isFavorited, setIsFavorited] = useState(false);
const [showShareModal, setShowShareModal] = useState(false);
const [showEditModal, setShowEditModal] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [copySuccess, setCopySuccess] = useState(false);
const [selectedTCG, setSelectedTCG] = useState('MTG');
// Edit form state
const [editForm, setEditForm] = useState({
name: '',
description: '',
isPublic: false,
image: '',
tags: []
});
// Filter states
const [searchQuery, setSearchQuery] = useState('');
const [selectedRarity, setSelectedRarity] = useState('All Rarities');
const [selectedType, setSelectedType] = useState('All Types');
const [groupBy, setGroupBy] = useState('Group by Game');
const [sortBy, setSortBy] = useState('Sort by Name');
const [viewMode, setViewMode] = useState('grid');
const [searchCards, setSearchCards] = useState('');
const [searchResults, setSearchResults] = useState([]);
const [showSearchResults, setShowSearchResults] = useState(false);
const [showUploadModal, setShowUploadModal] = useState(false);
// Card interaction states
const [selectedCards, setSelectedCards] = useState([]);
const [favoritedCards, setFavoritedCards] = useState(new Set());
const loadFavoritedCards = async () => {
try {
const token = localStorage.getItem('auth_token');
if (!token) return;
const response = await fetch('/api/favorites?type=card', {
headers: {
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
const favoriteIds = new Set(data.favorites.map(fav => parseInt(fav.item_id)));
setFavoritedCards(favoriteIds);
}
} catch (error) {
console.error('Error loading favorited cards:', error);
}
};
const checkIfFavorited = async () => {
try {
const response = await fetch(`/api/favorites?type=collection`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
if (response.ok) {
const data = await response.json();
const isFav = data.favorites.some(fav => fav.item_id === collection?.id);
setIsFavorited(isFav);
} else {
console.error('Failed to check favorites:', response.status);
}
} catch (error) {
console.error('Error checking favorites:', error);
}
};
const fetchCollectionData = async () => {
try {
// First try to fetch without authentication (for public collections)
let response = await fetch(`/api/collections/${identifier}`);
// If that fails and we have a user, try with authentication
if (!response.ok && user) {
response = await fetch(`/api/collections/${identifier}`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
}
if (response.ok) {
const data = await response.json();
// Check if we accessed via numeric ID and need to redirect to slug
if (data.slug && identifier !== data.slug && !isNaN(parseInt(identifier))) {
// Redirect to slug URL
router.replace(`/collection/${data.slug}`, undefined, { shallow: false });
return;
}
setCollection(data);
setEditForm({
name: data.name || '',
description: data.description || '',
isPublic: data.isPublic || false,
image: data.image || '',
tags: Array.isArray(data.tags) ? data.tags : (data.tags ? data.tags.split(',') : [])
});
// Fetch collection cards (try public first, then authenticated)
let cardsResponse = await fetch(`/api/collections/${identifier}/cards`);
if (!cardsResponse.ok && user) {
cardsResponse = await fetch(`/api/collections/${identifier}/cards`, {
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
}
if (cardsResponse.ok) {
const cardsData = await cardsResponse.json();
setCards(cardsData.cards || []);
}
// Check if collection is favorited
checkIfFavorited();
} else if (response.status === 401 || response.status === 403) {
// Collection is private and user is not authenticated/authorized
if (!user) {
router.push('/login');
} else {
// User is authenticated but doesn't have access
router.push('/collections');
}
}
} catch (error) {
console.error('Error fetching collection:', error);
} finally {
setLoading(false);
}
};
useEffect(() => {
if (identifier) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- load list when slug or auth changes
fetchCollectionData();
if (user) {
loadFavoritedCards();
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps -- reload when list slug or auth changes
}, [identifier, user]);
const handleEditCollection = async () => {
try {
const response = await fetch(`/api/collections/${identifier}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
name: editForm.name,
description: editForm.description,
isPublic: editForm.isPublic,
image: editForm.image,
tags: editForm.tags
})
});
if (response.ok) {
const updatedCollection = await response.json();
// If the name changed and we got a new slug, redirect
if (updatedCollection.slug && updatedCollection.slug !== identifier) {
router.push(`/collection/${updatedCollection.slug}`);
} else {
// Just refresh the data
fetchCollectionData();
}
setShowEditModal(false);
} else {
const error = await response.json();
alert(error.error || 'Failed to update list');
}
} catch (error) {
console.error('Error updating collection:', error);
alert('Network error. Please try again.');
}
};
const handleDeleteCollection = async () => {
try {
const response = await fetch(`/api/collections/${identifier}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
}
});
if (response.ok) {
router.push('/collections');
} else {
const error = await response.json();
alert(error.error || 'Failed to delete list');
}
} catch (error) {
console.error('Error deleting collection:', error);
alert('Network error. Please try again.');
}
};
const handleSearchCards = async (query) => {
if (query.length < 2) {
setSearchResults([]);
setShowSearchResults(false);
return;
}
try {
const response = await fetch(`/api/cards/search?q=${encodeURIComponent(query)}&limit=10`);
if (response.ok) {
const data = await response.json();
setSearchResults(data.cards || []);
setShowSearchResults(true);
}
} catch (error) {
console.error('Error searching cards:', error);
}
};
const handleAddCard = async (card) => {
try {
const response = await fetch(`/api/collections/${identifier}/cards`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
cardId: card.id,
quantity: 1
})
});
if (response.ok) {
setSearchCards('');
setShowSearchResults(false);
fetchCollectionData(); // Refresh the collection data
}
} catch (error) {
console.error('Error adding card:', error);
}
};
const toggleFavorite = async () => {
try {
if (isFavorited) {
// Remove from favorites
const response = await fetch('/api/favorites', {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
itemType: 'collection',
itemId: collection.id
})
});
if (response.ok) {
setIsFavorited(false);
} else {
console.error('Failed to remove favorite:', response.status);
}
} else {
// Add to favorites
const response = await fetch('/api/favorites', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
itemType: 'collection',
itemId: collection.id
})
});
if (response.ok) {
setIsFavorited(true);
} else {
console.error('Failed to add favorite:', response.status);
}
}
} catch (error) {
console.error('Error toggling favorite:', error);
}
};
const togglePublic = async () => {
try {
const response = await fetch(`/api/collections/${identifier}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
isPublic: !collection.isPublic
})
});
if (response.ok) {
setCollection(prev => ({
...prev,
isPublic: !prev.isPublic
}));
}
} catch (error) {
console.error('Error updating collection:', error);
}
};
const handleImageUpload = async (imageUrl) => {
try {
const response = await fetch(`/api/collections/${identifier}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
image: imageUrl
})
});
if (response.ok) {
setCollection(prev => ({
...prev,
image: imageUrl
}));
}
} catch (error) {
console.error('Error updating collection image:', error);
}
};
// Card interaction handlers
const handleToggleSelect = (card) => {
setSelectedCards(prev => {
const isSelected = prev.some(c => c.id === card.id);
if (isSelected) {
return prev.filter(c => c.id !== card.id);
} else {
return [...prev, card];
}
});
};
const handleToggleFavorite = async (card) => {
try {
const isFavorited = favoritedCards.has(card.id);
const method = isFavorited ? 'DELETE' : 'POST';
const response = await fetch('/api/favorites', {
method,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
},
body: JSON.stringify({
itemType: 'card',
itemId: card.id
})
});
if (response.ok) {
setFavoritedCards(prev => {
const newSet = new Set(prev);
if (isFavorited) {
newSet.delete(card.id);
} else {
newSet.add(card.id);
}
return newSet;
});
}
} catch (error) {
console.error('Error toggling favorite:', error);
}
};
const handleAddToCollection = (card) => {
// This would open a collection selection modal
console.log('Add to collection:', card);
};
const handleAddToDeck = (card) => {
// This would open a deck selection modal
console.log('Add to deck:', card);
};
const handleDownloadCSV = () => {
downloadCollectionCardsCsv(cards, collection?.name);
};
// Group cards by game
const groupedCards = cards.reduce((acc, card) => {
const game = card.game || 'Other';
if (!acc[game]) acc[game] = [];
acc[game].push(card);
return acc;
}, {});
// Get game display names and counts
const gameStats = {};
Object.keys(groupedCards).forEach(game => {
if (game && game !== 'Other') {
gameStats[game] = groupedCards[game].length;
}
});
const showInitialLoading = authLoading || loading;
return {
cards,
collection,
copySuccess,
editForm,
favoritedCards,
gameStats,
groupBy,
groupedCards,
handleAddCard,
handleAddToCollection,
handleAddToDeck,
handleDeleteCollection,
handleDownloadCSV,
handleEditCollection,
handleImageUpload,
handleSearchCards,
handleToggleFavorite,
handleToggleSelect,
identifier,
isFavorited,
loading,
router,
searchCards,
searchQuery,
searchResults,
selectedCards,
selectedRarity,
selectedTCG,
selectedType,
setCards,
setCollection,
setCopySuccess,
setEditForm,
setFavoritedCards,
setGroupBy,
setIsFavorited,
setLoading,
setSearchCards,
setSearchQuery,
setSearchResults,
setSelectedCards,
setSelectedRarity,
setSelectedTCG,
setSelectedType,
setShowDeleteModal,
setShowEditModal,
setShowSearchResults,
setShowShareModal,
setShowUploadModal,
setSortBy,
setViewMode,
showDeleteModal,
showEditModal,
showInitialLoading,
showSearchResults,
showShareModal,
showUploadModal,
sortBy,
viewMode
};
}