From 7230618ed5fda9964eadbbcbb49c8fe0efc09581 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Wed, 9 Apr 2025 12:24:39 +0700 Subject: [PATCH] react-virtualized to reduce lag --- .../sharedComps/ItemElements.tsx | 117 ++++++++++++++---- .../comps/SidePanelTabs/tabComps/SetsTab.tsx | 49 +++----- frontend/editor3d/package-lock.json | 52 +++++++- frontend/editor3d/package.json | 2 + 4 files changed, 166 insertions(+), 54 deletions(-) diff --git a/frontend/editor3d/app/pages/PageEnigma/comps/SidePanelTabs/sharedComps/ItemElements.tsx b/frontend/editor3d/app/pages/PageEnigma/comps/SidePanelTabs/sharedComps/ItemElements.tsx index e2c111352e..934b6f32c6 100644 --- a/frontend/editor3d/app/pages/PageEnigma/comps/SidePanelTabs/sharedComps/ItemElements.tsx +++ b/frontend/editor3d/app/pages/PageEnigma/comps/SidePanelTabs/sharedComps/ItemElements.tsx @@ -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 ( +
+ +
+ ); + }; + + 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 (
); } + if (items.length === 0 && !busy) { return (
@@ -65,13 +114,39 @@ export const ItemElements = ({
); } + return ( -
- {displayItems.map((item, index) => ( - - ))} +
+ + {({ 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 ( + + ); + }} + + {busy && hasMore && ( +
+ +
+ )}
); }; diff --git a/frontend/editor3d/app/pages/PageEnigma/comps/SidePanelTabs/tabComps/SetsTab.tsx b/frontend/editor3d/app/pages/PageEnigma/comps/SidePanelTabs/tabComps/SetsTab.tsx index e845103302..64d1d96bc2 100644 --- a/frontend/editor3d/app/pages/PageEnigma/comps/SidePanelTabs/tabComps/SetsTab.tsx +++ b/frontend/editor3d/app/pages/PageEnigma/comps/SidePanelTabs/tabComps/SetsTab.tsx @@ -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(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 = () => {
- {totalPages > 1 && ( - { - setCurrentPage(newPage); - }} - /> - )} setOpenUploadModal(false)} onSuccess={fetchUserObjects} diff --git a/frontend/editor3d/package-lock.json b/frontend/editor3d/package-lock.json index 293a635c71..b0fe76a9d1 100644 --- a/frontend/editor3d/package-lock.json +++ b/frontend/editor3d/package-lock.json @@ -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", diff --git a/frontend/editor3d/package.json b/frontend/editor3d/package.json index 7875ded0fd..777183e442 100644 --- a/frontend/editor3d/package.json +++ b/frontend/editor3d/package.json @@ -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",