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-router-dom": "^5.1.2",
|
||||||
"react-scripts": "^4.0.1",
|
"react-scripts": "^4.0.1",
|
||||||
"react-transition-group": "^4.3.0",
|
"react-transition-group": "^4.3.0",
|
||||||
"react-virtualized": "^9.22.2",
|
|
||||||
"simple-markdown": "^0.7.2",
|
"simple-markdown": "^0.7.2",
|
||||||
"xmlrpc": "^1.3.2"
|
"xmlrpc": "^1.3.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {
|
||||||
Wrap,
|
Wrap,
|
||||||
WrapItem,
|
WrapItem,
|
||||||
VStack,
|
VStack,
|
||||||
useBreakpointValue,
|
|
||||||
useToast,
|
useToast,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import {
|
import {
|
||||||
|
@ -32,19 +31,12 @@ import {
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import { useHistory, useParams } from "react-router-dom";
|
import { useHistory, useParams } from "react-router-dom";
|
||||||
import { useQuery, useLazyQuery, useMutation } from "@apollo/client";
|
import { useQuery, useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import { AutoSizer, Grid, WindowScroller } from "react-virtualized";
|
|
||||||
import SimpleMarkdown from "simple-markdown";
|
import SimpleMarkdown from "simple-markdown";
|
||||||
import DOMPurify from "dompurify";
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
import HangerSpinner from "./components/HangerSpinner";
|
import HangerSpinner from "./components/HangerSpinner";
|
||||||
import { Heading1, Heading2, Heading3 } from "./util";
|
import { Heading1, Heading2, Heading3 } from "./util";
|
||||||
import ItemCard, {
|
import ItemCard from "./components/ItemCard";
|
||||||
ItemBadgeList,
|
|
||||||
ItemKindBadge,
|
|
||||||
YouOwnThisBadge,
|
|
||||||
YouWantThisBadge,
|
|
||||||
getZoneBadges,
|
|
||||||
} from "./components/ItemCard";
|
|
||||||
import SupportOnly from "./WardrobePage/support/SupportOnly";
|
import SupportOnly from "./WardrobePage/support/SupportOnly";
|
||||||
import useSupport from "./WardrobePage/support/useSupport";
|
import useSupport from "./WardrobePage/support/useSupport";
|
||||||
import useCurrentUser from "./components/useCurrentUser";
|
import useCurrentUser from "./components/useCurrentUser";
|
||||||
|
@ -467,24 +459,13 @@ function ClosetList({ closetList, isCurrentUser, showHeading }) {
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
{sortedItems.length > 0 ? (
|
{sortedItems.length > 0 ? (
|
||||||
<VirtualizedItemCardList>
|
<Wrap spacing="4" justify="center">
|
||||||
{sortedItems.map((item) => (
|
{sortedItems.map((item) => (
|
||||||
<ItemCard
|
<WrapItem key={item.id}>
|
||||||
key={item.id}
|
<ItemCard item={item} variant="grid" />
|
||||||
item={item}
|
</WrapItem>
|
||||||
badges={
|
|
||||||
<ItemBadgeList>
|
|
||||||
<ItemKindBadge isNc={item.isNc} isPb={item.isPb} />
|
|
||||||
{hasYouOwnThisBadge(item) && <YouOwnThisBadge />}
|
|
||||||
{hasYouWantThisBadge(item) && <YouWantThisBadge />}
|
|
||||||
{getZoneBadges(item.allOccupiedZones, {
|
|
||||||
variant: "occupies",
|
|
||||||
})}
|
|
||||||
</ItemBadgeList>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
))}
|
))}
|
||||||
</VirtualizedItemCardList>
|
</Wrap>
|
||||||
) : (
|
) : (
|
||||||
<Box fontStyle="italic">This list is empty!</Box>
|
<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 = {
|
const unsafeMarkdownRules = {
|
||||||
autolink: SimpleMarkdown.defaultRules.autolink,
|
autolink: SimpleMarkdown.defaultRules.autolink,
|
||||||
br: SimpleMarkdown.defaultRules.br,
|
br: SimpleMarkdown.defaultRules.br,
|
||||||
|
|
|
@ -16,9 +16,74 @@ import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { safeImageUrl, useCommonStyles } from "../util";
|
import { safeImageUrl, useCommonStyles } from "../util";
|
||||||
|
|
||||||
function ItemCard({ item, badges, ...props }) {
|
function ItemCard({ item, badges, variant = "list", ...props }) {
|
||||||
const { brightBackground } = useCommonStyles();
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
as={Link}
|
as={Link}
|
||||||
|
@ -41,6 +106,9 @@ function ItemCard({ item, badges, ...props }) {
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unexpected ItemCard variant: ${variant}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ItemCardContent({
|
export function ItemCardContent({
|
||||||
|
@ -53,6 +121,7 @@ export function ItemCardContent({
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Box display="flex">
|
<Box display="flex">
|
||||||
|
<Box>
|
||||||
<Box flex="0 0 auto" marginRight="3">
|
<Box flex="0 0 auto" marginRight="3">
|
||||||
<ItemThumbnail
|
<ItemThumbnail
|
||||||
item={item}
|
item={item}
|
||||||
|
@ -61,6 +130,7 @@ export function ItemCardContent({
|
||||||
focusSelector={focusSelector}
|
focusSelector={focusSelector}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
</Box>
|
||||||
<Box flex="1 1 0" minWidth="0" marginTop="1px">
|
<Box flex="1 1 0" minWidth="0" marginTop="1px">
|
||||||
<ItemName
|
<ItemName
|
||||||
id={itemNameId}
|
id={itemNameId}
|
||||||
|
|
30
yarn.lock
30
yarn.lock
|
@ -6210,11 +6210,6 @@ clone-deep@^4.0.1:
|
||||||
kind-of "^6.0.2"
|
kind-of "^6.0.2"
|
||||||
shallow-clone "^3.0.0"
|
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:
|
co@^4.6.0:
|
||||||
version "4.6.0"
|
version "4.6.0"
|
||||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
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"
|
"@babel/runtime" "^7.8.7"
|
||||||
csstype "^2.6.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:
|
dom-serializer@0:
|
||||||
version "0.2.2"
|
version "0.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
|
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"
|
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339"
|
||||||
integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==
|
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:
|
react-refresh@^0.8.3:
|
||||||
version "0.8.3"
|
version "0.8.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"
|
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"
|
loose-envify "^1.4.0"
|
||||||
prop-types "^15.6.2"
|
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:
|
react@^17.0.1:
|
||||||
version "17.0.1"
|
version "17.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
|
resolved "https://registry.yarnpkg.com/react/-/react-17.0.1.tgz#6e0600416bd57574e3f86d92edba3d9008726127"
|
||||||
|
|
Loading…
Reference in a new issue