optimization
Virtualization for the card list -> Faster loading when searching
This commit is contained in:
Generated
+26
-4
@@ -9,7 +9,9 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4",
|
||||||
|
"react-virtuoso": "^4.18.3",
|
||||||
|
"react-window": "^2.2.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
@@ -1491,9 +1493,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.4.1",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||||
"integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
|
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -2253,6 +2255,26 @@
|
|||||||
"react": "^19.2.4"
|
"react": "^19.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-virtuoso": {
|
||||||
|
"version": "4.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.18.3.tgz",
|
||||||
|
"integrity": "sha512-fLz/peHAx4Eu0DLHurFEEI7Y6n5CqEoxBh04rgJM9yMuOJah2a9zWg/MUOmZLcp7zuWYorXq5+5bf3IRgkNvWg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16 || >=17 || >= 18 || >= 19",
|
||||||
|
"react-dom": ">=16 || >=17 || >= 18 || >=19"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-window": {
|
||||||
|
"version": "2.2.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-window/-/react-window-2.2.7.tgz",
|
||||||
|
"integrity": "sha512-SH5nvfUQwGHYyriDUAOt7wfPsfG9Qxd6OdzQxl5oQ4dsSsUicqQvjV7dR+NqZ4coY0fUn3w1jnC5PwzIUWEg5w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/resolve-from": {
|
"node_modules/resolve-from": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
|
||||||
|
|||||||
+3
-1
@@ -11,7 +11,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
"react-dom": "^19.2.4"
|
"react-dom": "^19.2.4",
|
||||||
|
"react-virtuoso": "^4.18.3",
|
||||||
|
"react-window": "^2.2.7"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
import React, { useContext, useState, memo } from 'react';
|
import React, { useContext, useState } from 'react';
|
||||||
import { CardContext } from '../../store/CardContext';
|
import { CardContext } from '../../store/CardContext';
|
||||||
|
|
||||||
function PrintingRow({ card_id, printing }) {
|
function PrintingRow({ card_id, printing }) {
|
||||||
const { ownedAmounts, updateAmount } = useContext(CardContext);
|
const { ownedAmounts, updateAmount } = useContext(CardContext);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const currentAmount = ownedAmounts[card_id]?.[printing.set_id]?.[printing.rarity_id] ?? printing.amount_owned ?? 0;
|
// Combined key for uniqueness
|
||||||
|
const key = `${printing.set_id}-${printing.rarity_id}`;
|
||||||
|
const currentAmount = ownedAmounts[card_id]?.[key] ?? printing.amount_owned ?? 0;
|
||||||
|
|
||||||
const updateBackend = async (newAmount) => {
|
const updateBackend = async (newAmount) => {
|
||||||
const { set_id, rarity_id } = printing;
|
const { set_id, rarity_id } = printing;
|
||||||
@@ -16,22 +18,39 @@ function PrintingRow({ card_id, printing }) {
|
|||||||
const response = await fetch('http://localhost:3000/collection/amount', {
|
const response = await fetch('http://localhost:3000/collection/amount', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ card_id, set_id, rarity_id, amount_owned: newAmount })
|
body: JSON.stringify({
|
||||||
|
card_id,
|
||||||
|
set_id,
|
||||||
|
rarity_id,
|
||||||
|
amount_owned: newAmount
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
updateAmount(card_id, set_id, rarity_id, newAmount);
|
// Update context using the combined key
|
||||||
|
updateAmount(card_id, key, newAmount);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// silently fail
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const increment = () => updateBackend(currentAmount + 1);
|
const increment = () => updateBackend(currentAmount + 1);
|
||||||
const decrement = () => { if (currentAmount > 0) updateBackend(currentAmount - 1); };
|
const decrement = () => {
|
||||||
|
if (currentAmount > 0) updateBackend(currentAmount - 1);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', padding: '0.25rem 0', opacity: loading ? 0.6 : 1 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
padding: '0.25rem 0',
|
||||||
|
opacity: loading ? 0.6 : 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
<span>{printing.set_name} {printing.rarity_name}</span>
|
<span>{printing.set_name} {printing.rarity_name}</span>
|
||||||
<div>
|
<div>
|
||||||
<button onClick={decrement} disabled={loading || currentAmount === 0}>–</button>
|
<button onClick={decrement} disabled={loading || currentAmount === 0}>–</button>
|
||||||
@@ -42,4 +61,21 @@ function PrintingRow({ card_id, printing }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(PrintingRow);
|
// ✅ Memoize PrintingRow for performance
|
||||||
|
export default React.memo(
|
||||||
|
PrintingRow,
|
||||||
|
(prevProps, nextProps) => {
|
||||||
|
// Re-render only if card_id changes or printing reference changes
|
||||||
|
// or if the current amount changed
|
||||||
|
const prevKey = `${prevProps.printing.set_id}-${prevProps.printing.rarity_id}`;
|
||||||
|
const nextKey = `${nextProps.printing.set_id}-${nextProps.printing.rarity_id}`;
|
||||||
|
const prevAmount = prevProps.printing.amount_owned;
|
||||||
|
const nextAmount = nextProps.printing.amount_owned;
|
||||||
|
|
||||||
|
return (
|
||||||
|
prevProps.card_id === nextProps.card_id &&
|
||||||
|
prevKey === nextKey &&
|
||||||
|
prevAmount === nextAmount
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useEffect, useState, useContext } from 'react';
|
import React, { useEffect, useState, useContext } from 'react';
|
||||||
|
import { Virtuoso } from 'react-virtuoso';
|
||||||
import CardRow from '../components/CardRow/CardRow';
|
import CardRow from '../components/CardRow/CardRow';
|
||||||
import SearchBar from '../components/SearchBar/SearchBar';
|
import SearchBar from '../components/SearchBar/SearchBar';
|
||||||
import { fetchCards } from '../services/api';
|
import { fetchCards } from '../services/api';
|
||||||
@@ -59,12 +60,16 @@ function HomePage() {
|
|||||||
return (
|
return (
|
||||||
<div style={{ display: 'flex', height: '100vh' }}>
|
<div style={{ display: 'flex', height: '100vh' }}>
|
||||||
{/* Left panel: card list */}
|
{/* Left panel: card list */}
|
||||||
<div style={{ flex: 2, borderRight: '1px solid #ccc', padding: '1rem', overflowY: 'auto' }}>
|
<div style={{ flex: 2, borderRight: '1px solid #ccc', padding: '1rem' }}>
|
||||||
<h2>Card List</h2>
|
<h2>Card List</h2>
|
||||||
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
|
<SearchBar searchTerm={searchTerm} setSearchTerm={setSearchTerm} />
|
||||||
{filteredCards.map(card => (
|
|
||||||
<CardRow key={card.id} card={card} />
|
{/* ✅ Virtualized list */}
|
||||||
))}
|
<Virtuoso
|
||||||
|
style={{ height: 'calc(100vh - 100px)' }} // Adjust for header/search bar
|
||||||
|
data={filteredCards}
|
||||||
|
itemContent={(index, card) => <CardRow key={card.id} card={card} />}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right panel: card image */}
|
{/* Right panel: card image */}
|
||||||
|
|||||||
Reference in New Issue
Block a user