fix(lint): clear ESLint baseline in pages/ #62
20 changed files with 228 additions and 170 deletions
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import dynamic from 'next/dynamic';
|
||||
|
|
@ -10,7 +11,8 @@ const CardEditor = () => {
|
|||
const { id } = router.query;
|
||||
|
||||
const [card, setCard] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [fetchLoading, setFetchLoading] = useState(false);
|
||||
const loading = id ? fetchLoading : false;
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [message, setMessage] = useState('');
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
|
@ -38,16 +40,8 @@ const CardEditor = () => {
|
|||
market_price: ''
|
||||
});
|
||||
|
||||
// Load card data if ID is provided
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
fetchCard(id);
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const fetchCard = async (cardId) => {
|
||||
setFetchLoading(true);
|
||||
try {
|
||||
const response = await fetch(`/api/cards/${cardId}`);
|
||||
if (response.ok) {
|
||||
|
|
@ -78,10 +72,18 @@ const CardEditor = () => {
|
|||
} catch (error) {
|
||||
setMessage('Error loading card: ' + error.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setFetchLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Load card data when editing an existing card
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- load card when route id is set
|
||||
fetchCard(id);
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
const searchCards = async (query) => {
|
||||
if (!query.trim()) {
|
||||
setSearchResults([]);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ function CardSubmissionsAdmin() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- mount fetch; setLoading runs inside async loader
|
||||
loadSubmissions();
|
||||
}, []);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useEffect } from 'react';
|
||||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect, useMemo } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Layout from '../../components/Layout';
|
||||
import { useAuth } from '../../lib/use-auth';
|
||||
|
|
@ -164,6 +165,18 @@ export default function CardDetail() {
|
|||
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 = {
|
||||
|
|
@ -355,7 +368,7 @@ export default function CardDetail() {
|
|||
Card Not Found
|
||||
</h2>
|
||||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
The card you're looking for doesn't exist.
|
||||
The card you're looking for doesn't exist.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => router.push('/cards')}
|
||||
|
|
@ -382,16 +395,11 @@ export default function CardDetail() {
|
|||
{/* Animated Particles Background */}
|
||||
{getParticleCount(card.rarity) > 0 && (
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
{Array.from({ length: getParticleCount(card.rarity) }).map((_, i) => (
|
||||
{particleStyles.map((style, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="absolute animate-float"
|
||||
style={{
|
||||
left: `${Math.random() * 100}%`,
|
||||
top: `${Math.random() * 100}%`,
|
||||
animationDelay: `${Math.random() * 3}s`,
|
||||
animationDuration: `${3 + Math.random() * 2}s`
|
||||
}}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className="w-1 h-1 rounded-full opacity-60"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -195,12 +196,15 @@ function AuthenticatedCards() {
|
|||
|
||||
// Initial load
|
||||
useEffect(() => {
|
||||
/* eslint-disable react-hooks/set-state-in-effect -- reset list state when filters change, then fetch */
|
||||
setPagination(prev => ({ ...prev, page: 1 }));
|
||||
setCards([]);
|
||||
setHasMore(true);
|
||||
hasMoreRef.current = true;
|
||||
fetchCards(false);
|
||||
loadFavoritedCards(); // Load user's favorites
|
||||
/* eslint-enable react-hooks/set-state-in-effect */
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- filter-driven reload; fetchCards closes over latest search state
|
||||
}, [selectedTCG, selectedRarity, selectedSet, selectedValueRange]);
|
||||
|
||||
// Handle search with debounce
|
||||
|
|
@ -328,6 +332,7 @@ function AuthenticatedCards() {
|
|||
observer.unobserve(sentinel);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- observer setup once; loadMoreCards uses refs for latest state
|
||||
}, []);
|
||||
|
||||
const getRarityColor = (rarity) => {
|
||||
|
|
@ -1407,7 +1412,7 @@ function Card3D({ card, viewMode, getRarityLabel, getRarityColor }) {
|
|||
{card.flavor_text && (
|
||||
<div>
|
||||
<div className="text-xs font-bold text-white mb-1">Quote</div>
|
||||
<div className="text-xs text-white italic leading-relaxed">"{card.flavor_text}"</div>
|
||||
<div className="text-xs text-white italic leading-relaxed">"{card.flavor_text}"</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- Binder/card images use external URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -52,14 +53,45 @@ export default function CollectionView() {
|
|||
const [selectedCards, setSelectedCards] = useState([]);
|
||||
const [favoritedCards, setFavoritedCards] = useState(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
if (identifier) {
|
||||
fetchCollectionData();
|
||||
if (user) {
|
||||
loadFavoritedCards();
|
||||
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);
|
||||
}
|
||||
}, [identifier, user]);
|
||||
} 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 {
|
||||
|
|
@ -128,24 +160,16 @@ export default function CollectionView() {
|
|||
}
|
||||
};
|
||||
|
||||
const checkIfFavorited = async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/favorites?type=collection`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${localStorage.getItem('auth_token')}`
|
||||
useEffect(() => {
|
||||
if (identifier) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- load list when slug or auth changes
|
||||
fetchCollectionData();
|
||||
if (user) {
|
||||
loadFavoritedCards();
|
||||
}
|
||||
});
|
||||
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);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- reload when list slug or auth changes
|
||||
}, [identifier, user]);
|
||||
|
||||
const handleEditCollection = async () => {
|
||||
try {
|
||||
|
|
@ -399,28 +423,6 @@ export default function CollectionView() {
|
|||
console.log('Add to deck:', card);
|
||||
};
|
||||
|
||||
// Load user's favorited cards
|
||||
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 handleDownloadCSV = () => {
|
||||
if (cards.length === 0) {
|
||||
alert('No cards to download');
|
||||
|
|
@ -993,7 +995,7 @@ export default function CollectionView() {
|
|||
Delete List
|
||||
</h2>
|
||||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
Are you sure you want to delete "{collectionDisplayName(collection)}"? This action cannot be undone and will permanently remove all cards and data associated with this list.
|
||||
Are you sure you want to delete "{collectionDisplayName(collection)}"? This action cannot be undone and will permanently remove all cards and data associated with this list.
|
||||
</p>
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Layout from '../components/Layout';
|
||||
|
|
@ -36,12 +37,6 @@ export default function Collections() {
|
|||
}
|
||||
}, [authLoading, user, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchCollections();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchCollections = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
|
@ -85,7 +80,16 @@ export default function Collections() {
|
|||
} 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)' },
|
||||
|
|
@ -678,7 +682,7 @@ export default function Collections() {
|
|||
placeholder="Enter image URL or upload later"
|
||||
/>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
|
||||
Add a custom thumbnail image, or we'll use your rarest cards
|
||||
Add a custom thumbnail image, or we'll use your rarest cards
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -803,7 +807,7 @@ export default function Collections() {
|
|||
List Created!
|
||||
</h2>
|
||||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
Your list "{createdCollection.name}" has been created successfully.
|
||||
Your list "{createdCollection.name}" has been created successfully.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -16,10 +17,6 @@ export default function CommunityCollections() {
|
|||
const [sortBy, setSortBy] = useState('name');
|
||||
|
||||
// Fetch collections on mount, regardless of auth status
|
||||
useEffect(() => {
|
||||
fetchPublicCollections();
|
||||
}, []);
|
||||
|
||||
const fetchPublicCollections = async () => {
|
||||
try {
|
||||
// Use public API endpoint that doesn't require authentication
|
||||
|
|
@ -57,6 +54,11 @@ export default function CommunityCollections() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- mount fetch; setLoading runs inside async loader
|
||||
fetchPublicCollections();
|
||||
}, []);
|
||||
|
||||
const sortOptions = [
|
||||
{ value: 'name', label: 'Name (A-Z)' },
|
||||
{ value: 'value', label: 'Value (High to Low)' },
|
||||
|
|
|
|||
|
|
@ -20,12 +20,6 @@ export default function Dashboard() {
|
|||
}
|
||||
}, [authLoading, user, router]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchCollections();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchCollections = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
|
@ -71,7 +65,16 @@ export default function Dashboard() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- load dashboard lists when user is available
|
||||
fetchCollections();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
;
|
||||
|
||||
return (
|
||||
<Layout user={user}>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -33,34 +34,6 @@ export default function DeckBuilder() {
|
|||
|
||||
const searchTimeoutRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && deckId) {
|
||||
fetchDeck();
|
||||
}
|
||||
}, [user, deckId]);
|
||||
|
||||
// Load initial cards when component mounts
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
searchCards();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
searchTimeoutRef.current = setTimeout(() => {
|
||||
searchCards();
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
if (searchTimeoutRef.current) {
|
||||
clearTimeout(searchTimeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, [searchQuery, filters]);
|
||||
|
||||
const fetchDeck = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
|
@ -130,6 +103,39 @@ export default function DeckBuilder() {
|
|||
}
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
// Load initial cards when component mounts
|
||||
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) => {
|
||||
// Commander format validation
|
||||
if (deck.format === 'Commander') {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -15,12 +16,6 @@ export default function DeckDetail() {
|
|||
const [loading, setLoading] = useState(true);
|
||||
const [groupBy, setGroupBy] = useState('type');
|
||||
|
||||
useEffect(() => {
|
||||
if (deckId) {
|
||||
fetchDeck();
|
||||
}
|
||||
}, [deckId]);
|
||||
|
||||
const fetchDeck = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
|
@ -41,7 +36,17 @@ export default function DeckDetail() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (deckId) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- load deck when route id changes
|
||||
fetchDeck();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- refetch when route deck id changes
|
||||
}, [deckId]);
|
||||
|
||||
;
|
||||
|
||||
const getDeckStats = () => {
|
||||
if (!deck?.cards) return { totalCards: 0, avgCmc: 0, colorCounts: {}, typeCounts: {} };
|
||||
|
|
@ -289,7 +294,7 @@ export default function DeckDetail() {
|
|||
<div className="text-center py-12">
|
||||
<div className="text-6xl mb-4">🃏</div>
|
||||
<h3 className="text-xl font-semibold text-text-primary mb-2">Empty Deck</h3>
|
||||
<p className="text-text-secondary">This deck doesn't have any cards yet</p>
|
||||
<p className="text-text-secondary">This deck doesn't have any cards yet</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
|
|
|
|||
|
|
@ -18,12 +18,6 @@ export default function Decks() {
|
|||
is_public: false
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
fetchDecks();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const fetchDecks = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
|
@ -44,7 +38,16 @@ export default function Decks() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- load decks when user is available
|
||||
fetchDecks();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
;
|
||||
|
||||
const handleCreateDeck = async (e) => {
|
||||
e.preventDefault();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
|
|
|
|||
|
|
@ -10,12 +10,6 @@ export default function AcceptInvite() {
|
|||
const [result, setResult] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
handleAcceptInvitation();
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const handleAcceptInvitation = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/invite/accept', {
|
||||
|
|
@ -38,7 +32,17 @@ export default function AcceptInvite() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- process invite token on mount
|
||||
handleAcceptInvitation();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- run once per invite token
|
||||
}, [token]);
|
||||
|
||||
;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
@ -69,7 +73,7 @@ export default function AcceptInvite() {
|
|||
Invitation Accepted!
|
||||
</h2>
|
||||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
You now have access to the list "{result.collection.name}".
|
||||
You now have access to the list "{result.collection.name}".
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
<button
|
||||
|
|
|
|||
|
|
@ -10,12 +10,6 @@ export default function DeclineInvite() {
|
|||
const [result, setResult] = useState(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
handleDeclineInvitation();
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const handleDeclineInvitation = async () => {
|
||||
try {
|
||||
const response = await fetch('/api/invite/decline', {
|
||||
|
|
@ -38,7 +32,17 @@ export default function DeclineInvite() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- process invite token on mount
|
||||
handleDeclineInvitation();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- run once per invite token
|
||||
}, [token]);
|
||||
|
||||
;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
|
|
@ -69,7 +73,7 @@ export default function DeclineInvite() {
|
|||
Invitation Declined
|
||||
</h2>
|
||||
<p className="mb-6" style={{ color: 'var(--text-secondary)' }}>
|
||||
You have declined the invitation to "{result.collection.name}".
|
||||
You have declined the invitation to "{result.collection.name}".
|
||||
You can always ask the list owner for another invitation if you change your mind.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ export default function Login() {
|
|||
|
||||
<div className="text-center">
|
||||
<p className="text-sm" style={{ color: 'var(--text-secondary)' }}>
|
||||
Don't have an account?{' '}
|
||||
Don't have an account?{' '}
|
||||
<Link href="/signup" className="font-medium gradient-text-flame hover:underline">
|
||||
Sign up here
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -152,9 +152,11 @@ export default function MyCards() {
|
|||
// Initial load
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- filter-driven reload via async fetchCards
|
||||
fetchCards();
|
||||
loadFavoritedCards();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- filter-driven reload; fetchCards closes over latest search state
|
||||
}, [user, searchQuery, selectedTCG, selectedRarity, selectedSet, selectedValueRange]);
|
||||
|
||||
// Infinite scroll
|
||||
|
|
@ -179,6 +181,7 @@ export default function MyCards() {
|
|||
observer.unobserve(sentinel);
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- rebind observer when card list length changes
|
||||
}, [cards]);
|
||||
|
||||
// Handle favorite toggle
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Layout from '../components/Layout';
|
||||
|
|
@ -30,11 +31,6 @@ export default function Profile() {
|
|||
favorite_games: []
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadUserProfile();
|
||||
loadUserStats();
|
||||
}, []);
|
||||
|
||||
const loadUserProfile = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
|
@ -90,6 +86,13 @@ export default function Profile() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- mount profile and stats load
|
||||
loadUserProfile();
|
||||
loadUserStats();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only profile and stats load
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (field, value) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
|
|
|
|||
|
|
@ -32,6 +32,10 @@ function loadSavedScannerSession() {
|
|||
}
|
||||
}
|
||||
|
||||
function createScanCardId() {
|
||||
return Date.now() + Math.random();
|
||||
}
|
||||
|
||||
function destinationActionKey(destination) {
|
||||
if (!destination) return 'pending';
|
||||
return destination.type === 'owned' ? 'owned' : destination.type;
|
||||
|
|
@ -72,14 +76,6 @@ export default function Scanner() {
|
|||
}
|
||||
}, [authLoading, user, router]);
|
||||
|
||||
// Load collections and decks
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
loadCollections();
|
||||
loadDecks();
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(
|
||||
|
|
@ -173,7 +169,7 @@ export default function Scanner() {
|
|||
} else {
|
||||
cardEntry = {
|
||||
...cardData,
|
||||
id: Date.now() + Math.random(),
|
||||
id: createScanCardId(),
|
||||
name: cardData.name,
|
||||
set: cardData.set,
|
||||
quantity: 1,
|
||||
|
|
|
|||
|
|
@ -40,10 +40,6 @@ export default function Settings() {
|
|||
const [message, setMessage] = useState({ type: '', text: '' });
|
||||
const [activeSection, setActiveSection] = useState('account');
|
||||
|
||||
useEffect(() => {
|
||||
loadSettings();
|
||||
}, []);
|
||||
|
||||
const loadSettings = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('auth_token');
|
||||
|
|
@ -71,7 +67,15 @@ export default function Settings() {
|
|||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- load settings on mount
|
||||
loadSettings();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- mount-only settings load
|
||||
}, []);
|
||||
|
||||
;
|
||||
|
||||
const handleSettingChange = (key, value) => {
|
||||
setSettings(prev => ({
|
||||
|
|
@ -615,7 +619,7 @@ export default function Settings() {
|
|||
))}
|
||||
</select>
|
||||
<p className="text-xs mt-1" style={{ color: 'var(--text-secondary)' }}>
|
||||
System will follow your device's theme preference
|
||||
System will follow your device's theme preference
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @next/next/no-img-element -- External or generated image URLs; next/image migration is out of scope. */
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -23,10 +24,6 @@ export default function Signup() {
|
|||
const [imageLoading, setImageLoading] = useState(false);
|
||||
|
||||
// Generate initial random avatar
|
||||
useEffect(() => {
|
||||
generateRandomAvatar();
|
||||
}, []);
|
||||
|
||||
const generateRandomAvatar = async () => {
|
||||
setImageLoading(true);
|
||||
try {
|
||||
|
|
@ -41,6 +38,11 @@ export default function Signup() {
|
|||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect -- seed signup avatar preview on mount
|
||||
generateRandomAvatar();
|
||||
}, []);
|
||||
|
||||
const handleInputChange = (field, value) => {
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
|
|
|
|||
Loading…
Reference in a new issue