Try a grid-based view for item lists
I took out virtualization for now too, I wanna see how this non-Chakra UI version, with fewer nodes and no tooltips etc, performs on large lists in production.
This commit is contained in:
parent
7a5a6b919b
commit
8567f9d4b8
4 changed files with 106 additions and 137 deletions
|
@ -40,7 +40,6 @@
|
|||
"react-router-dom": "^5.1.2",
|
||||
"react-scripts": "^4.0.1",
|
||||
"react-transition-group": "^4.3.0",
|
||||
"react-virtualized": "^9.22.2",
|
||||
"simple-markdown": "^0.7.2",
|
||||
"xmlrpc": "^1.3.2"
|
||||
},
|
||||
|
|
|
@ -18,7 +18,6 @@ import {
|
|||
Wrap,
|
||||
WrapItem,
|
||||
VStack,
|
||||
useBreakpointValue,
|
||||
useToast,
|
||||
} from "@chakra-ui/react";
|
||||
import {
|
||||
|
@ -32,19 +31,12 @@ import {
|
|||
import gql from "graphql-tag";
|
||||
import { useHistory, useParams } from "react-router-dom";
|
||||
import { useQuery, useLazyQuery, useMutation } from "@apollo/client";
|
||||
import { AutoSizer, Grid, WindowScroller } from "react-virtualized";
|
||||
import SimpleMarkdown from "simple-markdown";
|
||||
import DOMPurify from "dompurify";
|
||||
|
||||
import HangerSpinner from "./components/HangerSpinner";
|
||||
import { Heading1, Heading2, Heading3 } from "./util";
|
||||
import ItemCard, {
|
||||
ItemBadgeList,
|
||||
ItemKindBadge,
|
||||
YouOwnThisBadge,
|
||||
YouWantThisBadge,
|
||||
getZoneBadges,
|
||||
} from "./components/ItemCard";
|
||||
import ItemCard from "./components/ItemCard";
|
||||
import SupportOnly from "./WardrobePage/support/SupportOnly";
|
||||
import useSupport from "./WardrobePage/support/useSupport";
|
||||
import useCurrentUser from "./components/useCurrentUser";
|
||||
|
@ -467,24 +459,13 @@ function ClosetList({ closetList, isCurrentUser, showHeading }) {
|
|||
</Box>
|
||||
)}
|
||||
{sortedItems.length > 0 ? (
|
||||
<VirtualizedItemCardList>
|
||||
<Wrap spacing="4" justify="center">
|
||||
{sortedItems.map((item) => (
|
||||
<ItemCard
|
||||
key={item.id}
|
||||
item={item}
|
||||
badges={
|
||||
<ItemBadgeList>
|
||||
<ItemKindBadge isNc={item.isNc} isPb={item.isPb} />
|
||||
{hasYouOwnThisBadge(item) && <YouOwnThisBadge />}
|
||||
{hasYouWantThisBadge(item) && <YouWantThisBadge />}
|
||||
{getZoneBadges(item.allOccupiedZones, {
|
||||
variant: "occupies",
|
||||
})}
|
||||
</ItemBadgeList>
|
||||
}
|
||||
/>
|
||||
<WrapItem key={item.id}>
|
||||
<ItemCard item={item} variant="grid" />
|
||||
</WrapItem>
|
||||
))}
|
||||
</VirtualizedItemCardList>
|
||||
</Wrap>
|
||||
) : (
|
||||
<Box fontStyle="italic">This list is empty!</Box>
|
||||
)}
|
||||
|
@ -492,57 +473,6 @@ function ClosetList({ closetList, isCurrentUser, showHeading }) {
|
|||
);
|
||||
}
|
||||
|
||||
function VirtualizedItemCardList({ children }) {
|
||||
const columnCount = useBreakpointValue({ base: 1, md: 2, lg: 3 });
|
||||
const rowCount = Math.ceil(children.length / columnCount);
|
||||
|
||||
return (
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<WindowScroller>
|
||||
{({
|
||||
height,
|
||||
isScrolling,
|
||||
onChildScroll,
|
||||
scrollTop,
|
||||
registerChild,
|
||||
}) => (
|
||||
<Box
|
||||
// HACK: A mysterious invocation to force internal re-measuring!
|
||||
// Without this, most lists are very broken until the first
|
||||
// window resize event.
|
||||
// https://github.com/bvaughn/react-virtualized/issues/1324
|
||||
ref={(el) => registerChild(el)}
|
||||
>
|
||||
<Grid
|
||||
cellRenderer={({ key, rowIndex, columnIndex, style }) => (
|
||||
<Box
|
||||
key={key}
|
||||
style={style}
|
||||
paddingLeft={columnIndex > 0 ? "6" : "0"}
|
||||
>
|
||||
{children[rowIndex * columnCount + columnIndex]}
|
||||
</Box>
|
||||
)}
|
||||
columnCount={columnCount}
|
||||
columnWidth={width / columnCount}
|
||||
rowCount={rowCount}
|
||||
rowHeight={100}
|
||||
width={width}
|
||||
height={height}
|
||||
autoHeight
|
||||
isScrolling={isScrolling}
|
||||
onScroll={onChildScroll}
|
||||
scrollTop={scrollTop}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</WindowScroller>
|
||||
)}
|
||||
</AutoSizer>
|
||||
);
|
||||
}
|
||||
|
||||
const unsafeMarkdownRules = {
|
||||
autolink: SimpleMarkdown.defaultRules.autolink,
|
||||
br: SimpleMarkdown.defaultRules.br,
|
||||
|
|
|
@ -16,31 +16,99 @@ import { Link } from "react-router-dom";
|
|||
|
||||
import { safeImageUrl, useCommonStyles } from "../util";
|
||||
|
||||
function ItemCard({ item, badges, ...props }) {
|
||||
function ItemCard({ item, badges, variant = "list", ...props }) {
|
||||
const { brightBackground } = useCommonStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
as={Link}
|
||||
to={`/items/${item.id}`}
|
||||
display="block"
|
||||
p="2"
|
||||
boxShadow="lg"
|
||||
borderRadius="lg"
|
||||
background={brightBackground}
|
||||
transition="all 0.2s"
|
||||
className="item-card"
|
||||
width="100%"
|
||||
minWidth="0"
|
||||
{...props}
|
||||
>
|
||||
<ItemCardContent
|
||||
item={item}
|
||||
badges={badges}
|
||||
focusSelector=".item-card:hover &, .item-card:focus &"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
switch (variant) {
|
||||
case "grid":
|
||||
return (
|
||||
// ItemCard renders in large lists of 1k+ items, so we get a big perf
|
||||
// win by using Emotion directly instead of Chakra's styled-system Box.
|
||||
<ClassNames>
|
||||
{({ css }) => (
|
||||
<Link
|
||||
as={Link}
|
||||
to={`/items/${item.id}`}
|
||||
className={css`
|
||||
transition: all 0.2s;
|
||||
&:hover,
|
||||
&:focus {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
&:focus {
|
||||
box-shadow: ${theme.shadows.outline};
|
||||
outline: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
box-shadow: ${theme.shadows.md};
|
||||
border-radius: ${theme.radii.md};
|
||||
padding: ${theme.space["3"]};
|
||||
width: calc(80px + 2em);
|
||||
background: ${brightBackground};
|
||||
`}
|
||||
>
|
||||
<img
|
||||
src={safeImageUrl(item.thumbnailUrl)}
|
||||
alt={`Thumbnail art for ${item.name}`}
|
||||
width={80}
|
||||
height={80}
|
||||
/>
|
||||
<div
|
||||
className={css`
|
||||
/* Set min height to match a 2-line item name, so the cards
|
||||
* in a row aren't toooo differently sized... */
|
||||
margin-top: ${theme.space["1"]};
|
||||
font-size: ${theme.fontSizes.sm};
|
||||
min-height: 2.5em;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
`}
|
||||
// HACK: Emotion turns this into -webkit-display: -webkit-box?
|
||||
style={{ display: "-webkit-box" }}
|
||||
>
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</ClassNames>
|
||||
);
|
||||
case "list":
|
||||
return (
|
||||
<Box
|
||||
as={Link}
|
||||
to={`/items/${item.id}`}
|
||||
display="block"
|
||||
p="2"
|
||||
boxShadow="lg"
|
||||
borderRadius="lg"
|
||||
background={brightBackground}
|
||||
transition="all 0.2s"
|
||||
className="item-card"
|
||||
width="100%"
|
||||
minWidth="0"
|
||||
{...props}
|
||||
>
|
||||
<ItemCardContent
|
||||
item={item}
|
||||
badges={badges}
|
||||
focusSelector=".item-card:hover &, .item-card:focus &"
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
default:
|
||||
throw new Error(`Unexpected ItemCard variant: ${variant}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function ItemCardContent({
|
||||
|
@ -53,13 +121,15 @@ export function ItemCardContent({
|
|||
}) {
|
||||
return (
|
||||
<Box display="flex">
|
||||
<Box flex="0 0 auto" marginRight="3">
|
||||
<ItemThumbnail
|
||||
item={item}
|
||||
isActive={isWorn}
|
||||
isDisabled={isDisabled}
|
||||
focusSelector={focusSelector}
|
||||
/>
|
||||
<Box>
|
||||
<Box flex="0 0 auto" marginRight="3">
|
||||
<ItemThumbnail
|
||||
item={item}
|
||||
isActive={isWorn}
|
||||
isDisabled={isDisabled}
|
||||
focusSelector={focusSelector}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box flex="1 1 0" minWidth="0" marginTop="1px">
|
||||
<ItemName
|
||||
|
|
30
yarn.lock
30
yarn.lock
|
@ -6210,11 +6210,6 @@ clone-deep@^4.0.1:
|
|||
kind-of "^6.0.2"
|
||||
shallow-clone "^3.0.0"
|
||||
|
||||
clsx@^1.0.4:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
@ -7222,14 +7217,6 @@ dom-helpers@^5.0.1:
|
|||
"@babel/runtime" "^7.8.7"
|
||||
csstype "^2.6.7"
|
||||
|
||||
dom-helpers@^5.1.3:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.0.tgz#57fd054c5f8f34c52a3eeffdb7e7e93cd357d95b"
|
||||
integrity sha512-Ru5o9+V8CpunKnz5LGgWXkmrH/20cGKwcHwS4m73zIvs54CN9epEmT/HLqFJW3kXpakAFkEdzgy1hzlJe3E4OQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.7"
|
||||
csstype "^3.0.2"
|
||||
|
||||
dom-serializer@0:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
|
||||
|
@ -13498,11 +13485,6 @@ react-is@^17.0.1:
|
|||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
||||
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
||||
|
||||
react-lifecycles-compat@^3.0.4:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-refresh@^0.8.3:
|
||||
version "0.8.3"
|
||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
||||
|
@ -13648,18 +13630,6 @@ react-transition-group@^4.3.0:
|
|||
loose-envify "^1.4.0"
|
||||
prop-types "^15.6.2"
|
||||
|
||||
react-virtualized@^9.22.2:
|
||||
version "9.22.2"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.22.2.tgz#217a870bad91e5438f46f01a009e1d8ce1060a5a"
|
||||
integrity sha512-5j4h4FhxTdOpBKtePSs1yk6LDNT4oGtUwjT7Nkh61Z8vv3fTG/XeOf8J4li1AYaexOwTXnw0HFVxsV0GBUqwRw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.7.2"
|
||||
clsx "^1.0.4"
|
||||
dom-helpers "^5.1.3"
|
||||
loose-envify "^1.4.0"
|
||||
prop-types "^15.7.2"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
|
||||
react@^17.0.1:
|
||||
version "17.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
|
||||
|
|
Loading…
Reference in a new issue