diff --git a/app/src/components/layout/sidebarLeft/Assets.tsx b/app/src/components/layout/sidebarLeft/Assets.tsx deleted file mode 100644 index fbfe00b..0000000 --- a/app/src/components/layout/sidebarLeft/Assets.tsx +++ /dev/null @@ -1,474 +0,0 @@ -import React, { useEffect, useState } from "react"; -import Search from "../../ui/inputs/Search"; -import { getCategoryAsset } from "../../../services/factoryBuilder/asset/assets/getCategoryAsset"; -import { fetchAssets } from "../../../services/marketplace/fetchAssets"; -import { - useDecalStore, - useDroppedDecal, - useSelectedItem, -} from "../../../store/builder/store"; - -// images ------------------- -import vehicle from "../../../assets/image/categories/vehicles.png"; -import workStation from "../../../assets/image/categories/workStation.png"; -import machines from "../../../assets/image/categories/machines.png"; -import worker from "../../../assets/image/categories/worker.png"; -import storage from "../../../assets/image/categories/storage.png"; -import office from "../../../assets/image/categories/office.png"; -import safety from "../../../assets/image/categories/safety.png"; -import feneration from "../../../assets/image/categories/feneration.png"; -import decal from "../../../assets/image/categories/decal.png"; -import SkeletonUI from "../../templates/SkeletonUI"; -import { - AlertIcon, - ArrowIcon, - DecalInfoIcon, - HangTagIcon, - NavigationIcon, -} from "../../icons/ExportCommonIcons"; -import { getCategoryDecals } from "../../../services/factoryBuilder/asset/decals/getCategoryDecals"; -// ------------------------------------- - -interface AssetProp { - filename: string; - thumbnail?: string; - category: string; - description?: string; - tags: string; - url?: string; - uploadDate?: number; - isArchieve?: boolean; - animated?: boolean; - price?: number; - CreatedBy?: string; -} -interface CategoryListProp { - assetImage?: string; - assetName?: string; - categoryImage: string; - category: string; -} -const Assets: React.FC = () => { - const { setSelectedItem } = useSelectedItem(); - const { setDroppedDecal } = useDroppedDecal(); - const [searchValue, setSearchValue] = useState(""); - const [selectedCategory, setSelectedCategory] = useState(null); - const [categoryAssets, setCategoryAssets] = useState([]); - const [decalAsset, setDecalAsset] = useState(); - const [filtereredAssets, setFiltereredAssets] = useState( - [] - ); - const [categoryList, setCategoryList] = useState([]); - const [isLoading, setisLoading] = useState(false); // Loading state for assets - const { selectedSubCategory, setSelectedSubCategory } = useDecalStore(); - - const handleSearchChange = (value: string) => { - const searchTerm = searchValue - ? searchValue.toLowerCase() - : value.toLowerCase(); - setSearchValue(value); - - if (searchTerm.trim() === "" && !selectedCategory) { - setCategoryAssets([]); - - return; - } - if (selectedCategory === "Decals" || selectedSubCategory) { - const filteredModels = decalAsset?.filter((model: any) => - model.decalName?.toLowerCase().includes(searchTerm.toLowerCase()) - ); - setCategoryAssets(filteredModels); - } else { - const filteredModels = filtereredAssets?.filter((model) => { - if (!model?.tags || !model?.filename || !model?.category) return false; - if (searchTerm.startsWith(":") && searchTerm.length > 1) { - const tagSearchTerm = searchTerm.slice(1); - return model.tags.toLowerCase().includes(tagSearchTerm); - } else if (selectedCategory) { - return ( - model.category - .toLowerCase() - .includes(selectedCategory.toLowerCase()) && - model.filename.toLowerCase().includes(searchTerm) - ); - } else { - return model.filename.toLowerCase().includes(searchTerm); - } - }); - - setCategoryAssets(filteredModels); - } - }; - - useEffect(() => { - if (selectedCategory === "Decals") return; - const filteredAssets = async () => { - try { - const filt = await fetchAssets(); - setFiltereredAssets(filt); - } catch { - echo.error("Filter asset not found"); - } - }; - filteredAssets(); - }, [categoryAssets, selectedCategory]); - useEffect(() => { - if ( - (searchValue.trim() === "" && selectedCategory === "Decals") || - selectedSubCategory - ) { - const filteredModels = decalAsset?.filter((model: any) => - model.decalName?.toLowerCase().includes(searchValue.toLowerCase()) - ); - setCategoryAssets(filteredModels); - } - }, [selectedSubCategory, decalAsset, searchValue]); - - useEffect(() => { - setCategoryList([ - { category: "Fenestration", categoryImage: feneration }, - { category: "Decals", categoryImage: decal }, - { category: "Vehicles", categoryImage: vehicle }, - { category: "Workstation", categoryImage: workStation }, - { category: "Machines", categoryImage: machines }, - { category: "Workers", categoryImage: worker }, - { category: "Storage", categoryImage: storage }, - { category: "Safety", categoryImage: safety }, - { category: "Office", categoryImage: office }, - ]); - }, []); - - const fetchCategoryAssets = async (asset: any) => { - setisLoading(true); - setSelectedCategory(asset); - try { - const res = await getCategoryAsset(asset); - setCategoryAssets(res); - setFiltereredAssets(res); - setisLoading(false); // End loading - // eslint-disable-next-line - } catch (error) { - echo.error("failed to fetch assets"); - setisLoading(false); - } - - if (asset === "Decals") { - fetchCategoryDecals("Safety"); - } - }; - - const fetchCategoryDecals = async (asset: any) => { - setisLoading(true); - // setSelectedCategory(asset); - try { - const res = await getCategoryDecals(asset); - - setCategoryAssets(res); - setFiltereredAssets(res); - setDecalAsset(res); - setisLoading(false); // End loading - // eslint-disable-next-line - } catch (error) { - echo.error("failed to fetch assets"); - setisLoading(false); - } - }; - - const activeSubcategories = [ - { name: "Safety", icon: }, - { name: "Navigation", icon: }, - { name: "Branding", icon: }, - { name: "Informational", icon: }, - ]; - - return ( -
- -
-
- {(() => { - if (isLoading) { - return ; // Show skeleton when loading - } - if (searchValue) { - return ( -
-
-
-

- Results for{" "} - '{searchValue}' -

-
-
- {selectedCategory == "Decals" ? ( - <> -
- {activeSubcategories.map((cat, index) => ( -
{ - fetchCategoryDecals(cat.name); - setSelectedSubCategory(cat.name); - }} - > -
{cat.icon}
-
{cat.name}
-
- ))} -
- {categoryAssets?.map((asset: any, index: number) => ( -
- {asset.decalName} { - setSelectedItem({ - name: asset.decalName, - id: asset.id, - type: - asset.type === "undefined" - ? undefined - : asset.type, - category: asset.category, - // subType: asset.subType, - }); - }} - /> -
- {asset.decalName - .split("_") - .map( - (word: any) => - word.charAt(0).toUpperCase() + - word.slice(1) - ) - .join(" ")} -
-
- ))} - - ) : ( - categoryAssets?.map((asset: any, index: number) => ( -
- {asset.filename} { - setSelectedItem({ - name: asset.filename, - id: asset.AssetID, - type: - asset.type === "undefined" - ? undefined - : asset.type, - }); - }} - /> - -
- {asset.filename - .split("_") - .map( - (word: any) => - word.charAt(0).toUpperCase() + word.slice(1) - ) - .join(" ")} -
-
- )) - )} -
-
-
- ); - } - - if (selectedCategory) { - return ( -
-

- {selectedCategory} - -

- - {selectedCategory === "Decals" && ( - <> -
- {activeSubcategories.map((cat, index) => ( -
{ - fetchCategoryDecals(cat.name); - setSelectedSubCategory(cat.name); - }} - > -
{cat.icon}
-
{cat.name}
-
- ))} -
- - )} - {selectedCategory !== "Decals" && !selectedSubCategory ? ( -
- {categoryAssets?.map((asset: any, index: number) => ( -
- {asset.filename} { - setSelectedItem({ - name: asset.filename, - id: asset.AssetID, - type: - asset.type === "undefined" - ? undefined - : asset.type, - category: asset.category, - subType: asset.subType, - }); - }} - /> -
- {asset.filename - .split("_") - .map( - (word: any) => - word.charAt(0).toUpperCase() + word.slice(1) - ) - .join(" ")} -
-
- ))} - {categoryAssets.length === 0 && ( -
- 🚧 The asset shelf is empty. We're working on filling - it up! -
- )} -
- ) : ( -
- {categoryAssets?.map((asset: any, index: number) => ( -
- {asset.decalName} { - setDroppedDecal({ - category: asset.category, - decalName: asset.decalName, - decalImage: asset.decalImage, - decalId: asset.id, - }); - }} - /> -
- {asset.decalName - .split("_") - .map( - (word: any) => - word.charAt(0).toUpperCase() + word.slice(1) - ) - .join(" ")} -
-
- ))} - {categoryAssets.length === 0 && ( -
- 🚧 The asset shelf is empty. We're working on filling - it up! -
- )} -
- )} -
- ); - } - - return ( -
-

Categories

-
- {Array.from( - new Set(categoryList.map((asset) => asset.category)) - ).map((category, index) => { - const categoryInfo = categoryList.find( - (asset) => asset.category === category - ); - return ( -
{ - fetchCategoryAssets(category); - }} - > - {category} -
{category}
-
- ); - })} -
-
- ); - })()} -
-
-
- ); -}; - -export default Assets; diff --git a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx index fdd4f87..ad26627 100644 --- a/app/src/components/layout/sidebarLeft/SideBarLeft.tsx +++ b/app/src/components/layout/sidebarLeft/SideBarLeft.tsx @@ -3,7 +3,7 @@ import ToggleHeader from "../../ui/inputs/ToggleHeader"; import Outline from "./Outline"; import Header from "./Header"; import { useToggleStore } from "../../../store/useUIToggleStore"; -import Assets from "./Assets"; +import Assets from "./assetList/Assets"; import useModuleStore from "../../../store/useModuleStore"; import Widgets from "./visualization/widgets/Widgets"; import Templates from "../../../modules/visualization/template/Templates"; diff --git a/app/src/components/layout/sidebarLeft/assetList/Assets.tsx b/app/src/components/layout/sidebarLeft/assetList/Assets.tsx new file mode 100644 index 0000000..5294be5 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/Assets.tsx @@ -0,0 +1,177 @@ +import React, { useState, useEffect, useMemo, useCallback } from "react"; +import { useDecalStore } from "../../../../store/builder/store"; +import { getFilteredAssets } from "./assetsHelpers/filteredAssetsHelper"; +import { fetchCategoryDecals } from "./assetsHelpers/fetchDecalsHelper"; +import { + fetchAllAssets, + fetchCategoryAssets, +} from "./assetsHelpers/fetchAssetsHelper"; +import Search from "../../../ui/inputs/Search"; +import SkeletonUI from "../../../templates/SkeletonUI"; +import { RenderAsset } from "./assetsHelpers/renderAssetHelper"; +import { + ACTIVE_DECAL_SUBCATEGORIES, + CATEGORY_LIST, +} from "./assetsHelpers/constants"; +import { ArrowIcon } from "../../../icons/ExportCommonIcons"; + +const Assets: React.FC = () => { + const { selectedSubCategory, setSelectedSubCategory } = useDecalStore(); + const [searchValue, setSearchValue] = useState(null); + const [selectedCategory, setSelectedCategory] = useState(null); + const [assets, setAssets] = useState([]); + const [globalResults, setGlobalResults] = useState<(AssetProp | DecalProp)[]>( + [] + ); + const [isLoading, setIsLoading] = useState(false); + + const filteredAssets = useMemo( + () => + getFilteredAssets({ + assets, + searchValue, + selectedCategory, + selectedSubCategory, + }), + [assets, searchValue, selectedCategory, selectedSubCategory] + ); + + const handleFetchCategory = useCallback( + async (category: string) => { + setIsLoading(true); + setSelectedCategory(category); + if (category === "Decals") { + const res = await fetchCategoryDecals("Safety"); + setAssets(res); + setSelectedSubCategory("Safety"); + } else { + const res = await fetchCategoryAssets(category); + setAssets(res); + } + setIsLoading(false); + }, + [setSelectedSubCategory] + ); + + const fetchGlobalSearch = useCallback(async (term: string) => { + setIsLoading(true); + const allAssets = await fetchAllAssets(); + const lowerTerm = term.toLowerCase(); + const matches = allAssets.filter( + (a) => + a.filename.toLowerCase().includes(lowerTerm) || + a.tags?.toLowerCase().includes(lowerTerm) || + a.category?.toLowerCase().includes(lowerTerm) + ); + setGlobalResults(matches); + setIsLoading(false); + }, []); + + useEffect(() => { + if (!selectedCategory && searchValue?.trim()) + fetchGlobalSearch(searchValue); + else setGlobalResults([]); + }, [searchValue, selectedCategory, fetchGlobalSearch]); + + return ( +
+ +
+
+ {isLoading ? ( + + ) : searchValue || selectedCategory ? ( +
+ {selectedCategory ? ( + <> +

+ {selectedCategory} + +

+ {selectedCategory === "Decals" && ( +
+ {ACTIVE_DECAL_SUBCATEGORIES.map((cat) => ( +
{ + setIsLoading(true); + const res = await fetchCategoryDecals(cat.name); + setAssets(res); + setSelectedSubCategory(cat.name); + setIsLoading(false); + }} + > +
{cat.icon}
+
{cat.name}
+
+ ))} +
+ )} +
+ {filteredAssets.map((a, i) => ( + + ))} + {filteredAssets.length === 0 && ( +
🚧 No assets found
+ )} +
+ + ) : ( + <> +

Global Search Results

+
+ {globalResults.map((a, i) => ( + + ))} + {globalResults.length === 0 && ( +
🔎 No matches found
+ )} +
+ + )} +
+ ) : ( +
+

Categories

+
+ {CATEGORY_LIST.map((cat) => ( +
handleFetchCategory(cat.category)} + > + {cat.category} +
{cat.category}
+
+ ))} +
+
+ )} +
+
+
+ ); +}; + +export default Assets; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/constants.tsx b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/constants.tsx new file mode 100644 index 0000000..fe06ecc --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/constants.tsx @@ -0,0 +1,34 @@ +import vehicle from "../../../../../assets/image/categories/vehicles.png"; +import workStation from "../../../../../assets/image/categories/workStation.png"; +import machines from "../../../../../assets/image/categories/machines.png"; +import worker from "../../../../../assets/image/categories/worker.png"; +import storage from "../../../../../assets/image/categories/storage.png"; +import office from "../../../../../assets/image/categories/office.png"; +import safety from "../../../../../assets/image/categories/safety.png"; +import feneration from "../../../../../assets/image/categories/feneration.png"; +import decal from "../../../../../assets/image/categories/decal.png"; +import { + AlertIcon, + DecalInfoIcon, + HangTagIcon, + NavigationIcon, +} from "../../../../icons/ExportCommonIcons"; + +export const CATEGORY_LIST: CategoryListProp[] = [ + { category: "Fenestration", categoryImage: feneration }, + { category: "Decals", categoryImage: decal }, + { category: "Vehicles", categoryImage: vehicle }, + { category: "Workstation", categoryImage: workStation }, + { category: "Machines", categoryImage: machines }, + { category: "Workers", categoryImage: worker }, + { category: "Storage", categoryImage: storage }, + { category: "Safety", categoryImage: safety }, + { category: "Office", categoryImage: office }, +]; + +export const ACTIVE_DECAL_SUBCATEGORIES = [ + { name: "Safety", icon: }, + { name: "Navigation", icon: }, + { name: "Branding", icon: }, + { name: "Informational", icon: }, +]; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchAssetsHelper.ts b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchAssetsHelper.ts new file mode 100644 index 0000000..9fb678e --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchAssetsHelper.ts @@ -0,0 +1,22 @@ +import { getCategoryAsset } from "../../../../../services/factoryBuilder/asset/assets/getCategoryAsset"; +import { fetchAssets } from "../../../../../services/marketplace/fetchAssets"; + +export const fetchCategoryAssets = async (category: string): Promise => { + if (category === "Decals") return []; // handled separately + try { + const res = await getCategoryAsset(category); + return res; + } catch (err) { + console.error("Failed to fetch category assets", err); + return []; + } +}; + +export const fetchAllAssets = async (): Promise => { + try { + return await fetchAssets(); + } catch (err) { + console.error("Failed to fetch all assets", err); + return []; + } +}; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchDecalsHelper.ts b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchDecalsHelper.ts new file mode 100644 index 0000000..b0f1be4 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/fetchDecalsHelper.ts @@ -0,0 +1,11 @@ +import { getCategoryDecals } from "../../../../../services/factoryBuilder/asset/decals/getCategoryDecals"; + +export const fetchCategoryDecals = async (subcategory: string): Promise => { + try { + const res = await getCategoryDecals(subcategory); + return res; + } catch (err) { + console.error("Failed to fetch decals", err); + return []; + } +}; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/filteredAssetsHelper.ts b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/filteredAssetsHelper.ts new file mode 100644 index 0000000..38d2b39 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/filteredAssetsHelper.ts @@ -0,0 +1,34 @@ +interface FilterProps { + assets: AssetProp[] | DecalProp[]; + searchValue: string | null; + selectedCategory: string | null; + selectedSubCategory: string | null; +} + +export const getFilteredAssets = ({ + assets, + searchValue, + selectedCategory, + selectedSubCategory, +}: FilterProps) => { + const term = searchValue?.trim().toLowerCase(); + if (!term) return assets; + + if (selectedCategory === "Decals" || selectedSubCategory) { + return (assets as DecalProp[]).filter((a) => + a.decalName?.toLowerCase().includes(term) + ); + } + + return (assets as AssetProp[]).filter((a) => { + const tags = a.tags?.toLowerCase() ?? ""; + const filename = a.filename?.toLowerCase() ?? ""; + const category = a.category?.toLowerCase() ?? ""; + + if (term.startsWith(":")) return tags.includes(term.slice(1)); + if (selectedCategory) + return category.includes(selectedCategory.toLowerCase()) && filename.includes(term); + + return filename.includes(term); + }); +}; diff --git a/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/renderAssetHelper.tsx b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/renderAssetHelper.tsx new file mode 100644 index 0000000..2c27f79 --- /dev/null +++ b/app/src/components/layout/sidebarLeft/assetList/assetsHelpers/renderAssetHelper.tsx @@ -0,0 +1,58 @@ +import React from "react"; +import { useDroppedDecal, useSelectedItem } from "../../../../../store/builder/store"; + +export const RenderAsset: React.FC<{ asset: AssetProp | DecalProp; index: number }> = ({ asset, index }) => { + const { setSelectedItem } = useSelectedItem(); + const { setDroppedDecal } = useDroppedDecal(); + + if ("decalName" in asset) { + return ( +
+ {asset.decalName} + setDroppedDecal({ + category: asset.category, + decalName: asset.decalName, + decalImage: asset.decalImage, + decalId: asset.id, + }) + } + /> +
+ {asset.decalName + .split("_") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" ")} +
+
+ ); + } + + return ( +
+ {asset.filename} + setSelectedItem({ + name: asset.filename, + id: asset.AssetID, + type: asset.type === "undefined" ? undefined : asset.type, + category: asset.category, + subType: asset.subType, + }) + } + /> +
+ {asset.filename + .split("_") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" ")} +
+
+ ); +}; diff --git a/app/src/components/ui/inputs/Search.tsx b/app/src/components/ui/inputs/Search.tsx index ff9c6ef..73db02a 100644 --- a/app/src/components/ui/inputs/Search.tsx +++ b/app/src/components/ui/inputs/Search.tsx @@ -1,8 +1,8 @@ -import React, { ChangeEvent, useState } from "react"; +import React, { ChangeEvent, useEffect, useState } from "react"; import { CloseIcon, SearchIcon } from "../../icons/ExportCommonIcons"; interface SearchProps { - value?: string; // The current value of the search input + value?: string | null; // The current value of the search input placeholder?: string; // Placeholder text for the input onChange: (value: string) => void; // Callback function to handle input changes } @@ -22,7 +22,15 @@ const Search: React.FC = ({ onChange(newValue); // Call the onChange prop with the new value }; + useEffect(() => { + if (value === null) { + setInputValue(""); + handleBlur(); + } + }, [value]); + const handleClear = () => { + echo.warn("Search field cleared."); setInputValue(""); onChange(""); // Clear the input value }; @@ -48,7 +56,7 @@ const Search: React.FC = ({ { const { camera, controls } = useThree(); + + const isTextInput = (element: Element | null): boolean => + element instanceof HTMLInputElement || + element instanceof HTMLTextAreaElement || + element?.getAttribute("contenteditable") === "true"; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { @@ -21,6 +26,8 @@ const CameraShortcutsControls = () => { const dir = new THREE.Vector3().subVectors(camera.position, target).normalize(); + if (isTextInput(document.activeElement)) return; + switch (e.key) { case "1": // Front pos = new THREE.Vector3(0, 0, distance).add(target); diff --git a/app/src/types/uiTypes.d.ts b/app/src/types/uiTypes.d.ts new file mode 100644 index 0000000..16cb72c --- /dev/null +++ b/app/src/types/uiTypes.d.ts @@ -0,0 +1,29 @@ +interface AssetProp { + filename: string; + thumbnail?: string; + category: string; + description?: string; + tags: string; + url?: string; + uploadDate?: number; + isArchieve?: boolean; + animated?: boolean; + price?: number; + CreatedBy?: string; + AssetID?: string; + type?: string; + subType?: string; +} + +interface DecalProp { + id: string; + decalName: string; + decalImage: string; + category: string; + type?: string; +} + +interface CategoryListProp { + categoryImage: string; + category: string; +} \ No newline at end of file