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