Moves list index logic into a hook and view; CollectionsThumbnail is a shared presentational component. Co-authored-by: Cursor <cursoragent@cursor.com>
272 lines
8 KiB
JavaScript
272 lines
8 KiB
JavaScript
import { useState, useEffect } 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 useCollectionsPage({ user, authLoading }) {
|
|
const router = useRouter();
|
|
|
|
useEffect(() => {
|
|
if (!authLoading && !user) {
|
|
router.push('/login');
|
|
}
|
|
}, [authLoading, user, router]);
|
|
|
|
const [collections, setCollections] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showCreateModal, setShowCreateModal] = useState(false);
|
|
const [editingCollection, setEditingCollection] = useState(null);
|
|
const [searchQuery, setSearchQuery] = useState('');
|
|
const [sortBy, setSortBy] = useState('name'); // name, value, cardCount, createdAt
|
|
|
|
const [newCollection, setNewCollection] = useState({
|
|
name: '',
|
|
description: '',
|
|
isPublic: false,
|
|
image: '',
|
|
tags: []
|
|
});
|
|
const [showSuccessModal, setShowSuccessModal] = useState(false);
|
|
const [createdCollection, setCreatedCollection] = useState(null);
|
|
const [tagInput, setTagInput] = useState(''); // For creating new collections
|
|
const [editTagInput, setEditTagInput] = useState(''); // For editing collections
|
|
|
|
// Redirect to login if not authenticated
|
|
useEffect(() => {
|
|
if (!authLoading && !user) {
|
|
router.push('/login');
|
|
}
|
|
}, [authLoading, user, router]);
|
|
|
|
const fetchCollections = async () => {
|
|
try {
|
|
const token = localStorage.getItem('auth_token');
|
|
const headers = {
|
|
'Content-Type': 'application/json',
|
|
};
|
|
|
|
if (token) {
|
|
headers.Authorization = `Bearer ${token}`;
|
|
}
|
|
|
|
const response = await fetch('/api/collections', { headers });
|
|
|
|
if (response.ok) {
|
|
const data = await response.json();
|
|
|
|
// Fetch thumbnails for each collection
|
|
const collectionsWithThumbnails = await Promise.all(
|
|
data.map(async (collection) => {
|
|
try {
|
|
const identifier = collection.slug || collection.id;
|
|
const thumbnailResponse = await fetch(`/api/collections/${identifier}/thumbnails`, { headers });
|
|
if (thumbnailResponse.ok) {
|
|
const thumbnailData = await thumbnailResponse.json();
|
|
return { ...collection, thumbnails: thumbnailData.thumbnails };
|
|
}
|
|
return { ...collection, thumbnails: [] };
|
|
} catch (error) {
|
|
console.error(`Error fetching thumbnails for collection ${collection.id}:`, error);
|
|
return { ...collection, thumbnails: [] };
|
|
}
|
|
})
|
|
);
|
|
|
|
setCollections(collectionsWithThumbnails);
|
|
} else {
|
|
console.error('Failed to fetch collections');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error fetching collections:', error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (user) {
|
|
// eslint-disable-next-line react-hooks/set-state-in-effect -- load lists when user is available
|
|
fetchCollections();
|
|
}
|
|
}, [user]);
|
|
|
|
;
|
|
|
|
const sortOptions = [
|
|
{ value: 'name', label: 'Name (A-Z)' },
|
|
{ value: 'value', label: 'Value (High to Low)' },
|
|
{ value: 'cardCount', label: 'Card Count (High to Low)' },
|
|
{ value: 'createdAt', label: 'Date Created (Newest)' }
|
|
];
|
|
|
|
const sortCollections = (collections, sortBy) => {
|
|
return [...collections].sort((a, b) => {
|
|
switch (sortBy) {
|
|
case 'name':
|
|
return a.name.localeCompare(b.name);
|
|
case 'value':
|
|
return b.value - a.value;
|
|
case 'cardCount':
|
|
return b.cardCount - a.cardCount;
|
|
case 'createdAt':
|
|
return new Date(b.createdAt) - new Date(a.createdAt);
|
|
default:
|
|
return 0;
|
|
}
|
|
});
|
|
};
|
|
|
|
const filteredCollections = collections.filter(collection => {
|
|
const matchesSearch = collection.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
|
collection.description.toLowerCase().includes(searchQuery.toLowerCase());
|
|
return matchesSearch;
|
|
});
|
|
|
|
const sortedCollections = sortCollections(filteredCollections, sortBy);
|
|
|
|
const handleCreateCollection = async () => {
|
|
try {
|
|
const response = await fetch('/api/collections', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: newCollection.name,
|
|
description: newCollection.description,
|
|
isPublic: newCollection.isPublic,
|
|
image: newCollection.image,
|
|
tags: newCollection.tags
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
const createdCollection = await response.json();
|
|
setCreatedCollection(createdCollection);
|
|
setShowCreateModal(false);
|
|
setShowSuccessModal(true);
|
|
|
|
// Reset form
|
|
setNewCollection({
|
|
name: '',
|
|
description: '',
|
|
isPublic: false,
|
|
image: '',
|
|
tags: []
|
|
});
|
|
setTagInput(''); // Clear tag input
|
|
|
|
// Refresh collections list
|
|
fetchCollections();
|
|
} else {
|
|
const error = await response.json();
|
|
alert(error.error || 'Failed to create list');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error creating collection:', error);
|
|
alert('Network error. Please try again.');
|
|
}
|
|
};
|
|
|
|
const handleUpdateCollection = async () => {
|
|
try {
|
|
const response = await fetch(`/api/collections/${editingCollection.slug || editingCollection.id}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
body: JSON.stringify({
|
|
name: editingCollection.name,
|
|
description: editingCollection.description,
|
|
isPublic: editingCollection.isPublic,
|
|
image: editingCollection.image,
|
|
tags: editingCollection.tags
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Refresh collections after update
|
|
fetchCollections();
|
|
setEditingCollection(null);
|
|
setEditTagInput(''); // Clear the tag input
|
|
} 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 (collectionId) => {
|
|
if (confirm('Are you sure you want to delete this list? This action cannot be undone.')) {
|
|
try {
|
|
const response = await fetch(`/api/collections/${collectionId}`, {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
if (response.ok) {
|
|
fetchCollections(); // Refresh the list
|
|
} 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 formatCurrency = (amount) => {
|
|
return new Intl.NumberFormat('en-US', {
|
|
style: 'currency',
|
|
currency: 'USD'
|
|
}).format(amount);
|
|
};
|
|
|
|
const formatDate = (dateString) => {
|
|
return new Date(dateString).toLocaleDateString();
|
|
};
|
|
|
|
const showInitialLoading = authLoading || loading;
|
|
|
|
return {
|
|
collections,
|
|
createdCollection,
|
|
editTagInput,
|
|
editingCollection,
|
|
filteredCollections,
|
|
formatCurrency,
|
|
formatDate,
|
|
handleCreateCollection,
|
|
handleDeleteCollection,
|
|
handleUpdateCollection,
|
|
loading,
|
|
newCollection,
|
|
router,
|
|
searchQuery,
|
|
setCollections,
|
|
setCreatedCollection,
|
|
setEditTagInput,
|
|
setEditingCollection,
|
|
setLoading,
|
|
setNewCollection,
|
|
setSearchQuery,
|
|
setShowCreateModal,
|
|
setShowSuccessModal,
|
|
setSortBy,
|
|
setTagInput,
|
|
showCreateModal,
|
|
showInitialLoading,
|
|
showSuccessModal,
|
|
sortBy,
|
|
sortCollections,
|
|
sortOptions,
|
|
sortedCollections,
|
|
tagInput
|
|
};
|
|
}
|