mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
react-virtualized to reduce lag
This commit is contained in:
1 parent
1ae844259f
commit
7230618ed5
4 files changed
+166
-54
No files matched your search
+96
-21
@@ -4,14 +4,16 @@ import { dndSidePanelWidth, sidePanelWidth } from "~/pages/PageEnigma/signals";
|
||||
import { H4, P, LoadingDots } from "~/components";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEmptySet } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { AutoSizer, Grid } from "react-virtualized";
|
||||
import "react-virtualized/styles.css";
|
||||
|
||||
interface Props {
|
||||
busy?: boolean;
|
||||
className?: string;
|
||||
debug?: string;
|
||||
currentPage?: number;
|
||||
pageSize?: number;
|
||||
items: MediaItem[];
|
||||
onLoadMore?: () => void;
|
||||
hasMore?: boolean;
|
||||
}
|
||||
|
||||
export const ItemElements = ({
|
||||
@@ -19,40 +21,87 @@ export const ItemElements = ({
|
||||
className,
|
||||
debug,
|
||||
items,
|
||||
currentPage,
|
||||
pageSize = 20,
|
||||
onLoadMore,
|
||||
hasMore,
|
||||
}: Props) => {
|
||||
const displayWidth =
|
||||
dndSidePanelWidth.value > -1
|
||||
? dndSidePanelWidth.value
|
||||
: sidePanelWidth.value;
|
||||
|
||||
const displayItems =
|
||||
currentPage !== undefined
|
||||
? items.slice(currentPage * pageSize, (currentPage + 1) * pageSize)
|
||||
: items;
|
||||
const GRID_GAP = 8;
|
||||
const ASPECT_RATIO = 4.5 / 5;
|
||||
const TEXT_HEIGHT = 24;
|
||||
|
||||
function getGridColumnsClass(displayWidth: number): string {
|
||||
const columnCount = getGridColumns(displayWidth);
|
||||
const rowCount = Math.ceil(items.length / columnCount);
|
||||
|
||||
function getGridColumns(displayWidth: number): number {
|
||||
if (displayWidth <= 280) {
|
||||
return "grid-cols-2";
|
||||
return 2;
|
||||
} else if (displayWidth <= 360) {
|
||||
return "grid-cols-3";
|
||||
return 3;
|
||||
} else if (displayWidth <= 440) {
|
||||
return "grid-cols-4";
|
||||
return 4;
|
||||
} else {
|
||||
return "grid-cols-4";
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
const gridColumnsClass = getGridColumnsClass(displayWidth);
|
||||
const cellRenderer = ({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
key,
|
||||
style,
|
||||
}: {
|
||||
columnIndex: number;
|
||||
rowIndex: number;
|
||||
key: string;
|
||||
style: React.CSSProperties;
|
||||
}) => {
|
||||
const index = rowIndex * columnCount + columnIndex;
|
||||
if (index >= items.length) return null;
|
||||
|
||||
if (busy) {
|
||||
const adjustedStyle = {
|
||||
...style,
|
||||
padding: GRID_GAP / 2,
|
||||
height: style.height as number,
|
||||
boxSizing: "border-box" as const,
|
||||
};
|
||||
|
||||
return (
|
||||
<div key={key} style={adjustedStyle}>
|
||||
<ItemElement debug={debug} item={items[index]} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleScroll = ({
|
||||
clientHeight,
|
||||
scrollHeight,
|
||||
scrollTop,
|
||||
}: {
|
||||
clientHeight: number;
|
||||
scrollHeight: number;
|
||||
scrollTop: number;
|
||||
}) => {
|
||||
if (!onLoadMore || !hasMore || busy) return;
|
||||
|
||||
const threshold = 100; // pixels from bottom
|
||||
const bottom = scrollHeight - clientHeight - scrollTop;
|
||||
if (bottom < threshold) {
|
||||
onLoadMore();
|
||||
}
|
||||
};
|
||||
|
||||
if (busy && items.length === 0) {
|
||||
return (
|
||||
<div className="flex h-full w-full">
|
||||
<LoadingDots className="bg-transparent" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length === 0 && !busy) {
|
||||
return (
|
||||
<div className="flex h-full w-full flex-col items-center justify-center text-center">
|
||||
@@ -65,13 +114,39 @@ export const ItemElements = ({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`grid ${gridColumnsClass} gap-2.5 ${className ? " " + className : ""}`}
|
||||
>
|
||||
{displayItems.map((item, index) => (
|
||||
<ItemElement debug={debug} key={index} item={item} />
|
||||
))}
|
||||
<div className={`h-full w-full ${className || ""}`}>
|
||||
<AutoSizer className="w-full">
|
||||
{({ height, width }) => {
|
||||
const availableWidth = width - GRID_GAP * (columnCount - 1);
|
||||
const cellWidth = Math.floor(availableWidth / columnCount);
|
||||
const imageHeight = Math.floor(cellWidth / ASPECT_RATIO);
|
||||
const cellHeight = imageHeight + TEXT_HEIGHT;
|
||||
|
||||
return (
|
||||
<Grid
|
||||
cellRenderer={cellRenderer}
|
||||
columnCount={columnCount}
|
||||
columnWidth={cellWidth}
|
||||
height={height}
|
||||
rowCount={rowCount}
|
||||
rowHeight={cellHeight}
|
||||
width={width}
|
||||
style={{ outline: "none" }}
|
||||
columnGap={GRID_GAP}
|
||||
rowGap={GRID_GAP}
|
||||
overscanRowCount={2}
|
||||
onScroll={handleScroll}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</AutoSizer>
|
||||
{busy && hasMore && (
|
||||
<div className="flex w-full justify-center py-4">
|
||||
<LoadingDots className="bg-transparent" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { faCirclePlus } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
import { usePosthogFeatureFlag } from "~/hooks/usePosthogFeatureFlag";
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
import {
|
||||
Button,
|
||||
FilterButtons,
|
||||
Pagination,
|
||||
SearchFilter,
|
||||
UploadModal3D,
|
||||
} from "~/components";
|
||||
@@ -33,6 +32,7 @@ import {
|
||||
} from "../hooks";
|
||||
|
||||
const filterEngineCategories = [FilterEngineCategories.LOCATION];
|
||||
const PAGE_SIZE = 42; // Load 2 pages worth of items at a time
|
||||
|
||||
export const SetsTab = () => {
|
||||
const showSearchObjectComponent = usePosthogFeatureFlag(
|
||||
@@ -40,6 +40,7 @@ export const SetsTab = () => {
|
||||
);
|
||||
|
||||
const [openUploadModal, setOpenUploadModal] = useState(false);
|
||||
const [loadedItemCount, setLoadedItemCount] = useState(PAGE_SIZE);
|
||||
|
||||
const { userObjects, userFetchStatus, fetchUserObjects } = useUserObjects({
|
||||
filterEngineCategories: filterEngineCategories,
|
||||
@@ -78,15 +79,11 @@ export const SetsTab = () => {
|
||||
const displayedItems =
|
||||
selectedFilter === AssetFilterOption.FEATURED
|
||||
? searchTermForFeaturedObjects
|
||||
? featuredObjectsSearchResults ?? []
|
||||
: featuredObjects ?? []
|
||||
? (featuredObjectsSearchResults ?? [])
|
||||
: (featuredObjects ?? [])
|
||||
: searchTermForUserObjects
|
||||
? userObjectsSearchResults ?? []
|
||||
: userObjects ?? [];
|
||||
|
||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||
const pageSize = 21;
|
||||
const totalPages = Math.ceil(displayedItems.length / pageSize);
|
||||
? (userObjectsSearchResults ?? [])
|
||||
: (userObjects ?? []);
|
||||
|
||||
const isFetching = isAnyStatusFetching([
|
||||
userFetchStatus,
|
||||
@@ -95,17 +92,17 @@ export const SetsTab = () => {
|
||||
userObjectsSearchFetchStatus,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTermForUserObjects.length > 0) {
|
||||
setCurrentPage(0);
|
||||
const handleLoadMore = useCallback(() => {
|
||||
if (!isFetching && loadedItemCount < displayedItems.length) {
|
||||
setLoadedItemCount((prev) =>
|
||||
Math.min(prev + PAGE_SIZE, displayedItems.length),
|
||||
);
|
||||
}
|
||||
}, [searchTermForUserObjects]);
|
||||
}, [isFetching, loadedItemCount, displayedItems.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (searchTermForFeaturedObjects.length > 0) {
|
||||
setCurrentPage(0);
|
||||
}
|
||||
}, [searchTermForFeaturedObjects]);
|
||||
setLoadedItemCount(PAGE_SIZE);
|
||||
}, [searchTermForUserObjects, searchTermForFeaturedObjects, selectedFilter]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -152,21 +149,11 @@ export const SetsTab = () => {
|
||||
<ItemElements
|
||||
busy={isFetching}
|
||||
debug="sets tab"
|
||||
currentPage={currentPage}
|
||||
pageSize={pageSize}
|
||||
items={displayedItems}
|
||||
items={displayedItems.slice(0, loadedItemCount)}
|
||||
onLoadMore={handleLoadMore}
|
||||
hasMore={loadedItemCount < displayedItems.length}
|
||||
/>
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<Pagination
|
||||
className="-mt-4 px-4"
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={(newPage: number) => {
|
||||
setCurrentPage(newPage);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<UploadModal3D
|
||||
onClose={() => setOpenUploadModal(false)}
|
||||
onSuccess={fetchUserObjects}
|
||||
|
||||
Generated
+50
-2
@@ -25,6 +25,7 @@
|
||||
"@remix-run/serve": "^2.8.1",
|
||||
"@tweenjs/tween.js": "^18.6.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"@zip.js/zip.js": "^2.7.51",
|
||||
"ammo.js": "^0.0.10",
|
||||
"ccapture.js": "github:aceslowman/ccapture.js#npm-fix",
|
||||
@@ -44,6 +45,7 @@
|
||||
"react-drag-drop-files": "^2.3.10",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-image-crop": "^11.0.5",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"terser": "^5.30.3",
|
||||
"three": "^0.162.0",
|
||||
@@ -4589,14 +4591,12 @@
|
||||
"version": "15.7.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
|
||||
"integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.18",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz",
|
||||
"integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
@@ -4633,6 +4633,15 @@
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-virtualized": {
|
||||
"version": "9.22.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.22.2.tgz",
|
||||
"integrity": "sha512-0Eg/ME3OHYWGxs+/n4VelfYrhXssireZaa1Uqj5SEkTpSaBu5ctFGOCVxcOqpGXRiEdrk/7uho9tlZaryCIjHA==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz",
|
||||
@@ -7195,6 +7204,14 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/co": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
|
||||
@@ -7996,6 +8013,15 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-helpers": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.8.7",
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/domexception": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz",
|
||||
@@ -18824,6 +18850,11 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-lifecycles-compat": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.14.2",
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz",
|
||||
@@ -18874,6 +18905,23 @@
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-virtualized": {
|
||||
"version": "9.22.6",
|
||||
"resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.22.6.tgz",
|
||||
"integrity": "sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==",
|
||||
"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"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
"@remix-run/serve": "^2.8.1",
|
||||
"@tweenjs/tween.js": "^18.6.4",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/react-virtualized": "^9.22.2",
|
||||
"@zip.js/zip.js": "^2.7.51",
|
||||
"ammo.js": "^0.0.10",
|
||||
"ccapture.js": "github:aceslowman/ccapture.js#npm-fix",
|
||||
@@ -56,6 +57,7 @@
|
||||
"react-drag-drop-files": "^2.3.10",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-image-crop": "^11.0.5",
|
||||
"react-virtualized": "^9.22.6",
|
||||
"tailwind-merge": "^2.2.2",
|
||||
"terser": "^5.30.3",
|
||||
"three": "^0.162.0",
|
||||
|
||||
Reference in new issue
Block a user