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:
Emi Matchu 2021-01-14 04:58:15 -08:00
parent 7a5a6b919b
commit 8567f9d4b8
4 changed files with 106 additions and 137 deletions

View file

@ -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"
},

View file

@ -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,

View file

@ -16,9 +16,74 @@ 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();
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}
@ -41,6 +106,9 @@ function ItemCard({ item, badges, ...props }) {
/>
</Box>
);
default:
throw new Error(`Unexpected ItemCard variant: ${variant}`);
}
}
export function ItemCardContent({
@ -53,6 +121,7 @@ export function ItemCardContent({
}) {
return (
<Box display="flex">
<Box>
<Box flex="0 0 auto" marginRight="3">
<ItemThumbnail
item={item}
@ -61,6 +130,7 @@ export function ItemCardContent({
focusSelector={focusSelector}
/>
</Box>
</Box>
<Box flex="1 1 0" minWidth="0" marginTop="1px">
<ItemName
id={itemNameId}

View file

@ -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"