refactor: reorganize asset management components and enhance search functionality
This commit is contained in:
@@ -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<string>("");
|
|
||||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
|
||||||
const [categoryAssets, setCategoryAssets] = useState<AssetProp[]>([]);
|
|
||||||
const [decalAsset, setDecalAsset] = useState<any>();
|
|
||||||
const [filtereredAssets, setFiltereredAssets] = useState<AssetProp[] | []>(
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
const [categoryList, setCategoryList] = useState<CategoryListProp[]>([]);
|
|
||||||
const [isLoading, setisLoading] = useState<boolean>(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: <AlertIcon /> },
|
|
||||||
{ name: "Navigation", icon: <NavigationIcon /> },
|
|
||||||
{ name: "Branding", icon: <HangTagIcon /> },
|
|
||||||
{ name: "Informational", icon: <DecalInfoIcon /> },
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="assets-container-main">
|
|
||||||
<Search onChange={handleSearchChange} value={searchValue} />
|
|
||||||
<div className="assets-list-section">
|
|
||||||
<section>
|
|
||||||
{(() => {
|
|
||||||
if (isLoading) {
|
|
||||||
return <SkeletonUI type="asset" />; // Show skeleton when loading
|
|
||||||
}
|
|
||||||
if (searchValue) {
|
|
||||||
return (
|
|
||||||
<div className="assets-result">
|
|
||||||
<div className="assets-wrapper">
|
|
||||||
<div className="searched-content">
|
|
||||||
<p>
|
|
||||||
Results for{" "}
|
|
||||||
<span className="search-for">'{searchValue}'</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="assets-container">
|
|
||||||
{selectedCategory == "Decals" ? (
|
|
||||||
<>
|
|
||||||
<div className="catogory-asset-filter">
|
|
||||||
{activeSubcategories.map((cat, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`catogory-asset-filter-wrapper ${
|
|
||||||
selectedSubCategory === cat.name
|
|
||||||
? "active"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
fetchCategoryDecals(cat.name);
|
|
||||||
setSelectedSubCategory(cat.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="sub-catagory">{cat.icon}</div>
|
|
||||||
<div className="sub-catagory">{cat.name}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{categoryAssets?.map((asset: any, index: number) => (
|
|
||||||
<div
|
|
||||||
key={`${index}-${asset}`}
|
|
||||||
className="assets"
|
|
||||||
id={asset.decalName}
|
|
||||||
title={asset.decalName}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={asset?.decalImage}
|
|
||||||
alt={asset.decalName}
|
|
||||||
className="asset-image"
|
|
||||||
onPointerDown={() => {
|
|
||||||
setSelectedItem({
|
|
||||||
name: asset.decalName,
|
|
||||||
id: asset.id,
|
|
||||||
type:
|
|
||||||
asset.type === "undefined"
|
|
||||||
? undefined
|
|
||||||
: asset.type,
|
|
||||||
category: asset.category,
|
|
||||||
// subType: asset.subType,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="asset-name">
|
|
||||||
{asset.decalName
|
|
||||||
.split("_")
|
|
||||||
.map(
|
|
||||||
(word: any) =>
|
|
||||||
word.charAt(0).toUpperCase() +
|
|
||||||
word.slice(1)
|
|
||||||
)
|
|
||||||
.join(" ")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
categoryAssets?.map((asset: any, index: number) => (
|
|
||||||
<div
|
|
||||||
key={`${index}-${asset.filename}`}
|
|
||||||
className="assets"
|
|
||||||
id={asset.filename}
|
|
||||||
title={asset.filename}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={asset?.thumbnail}
|
|
||||||
alt={asset.filename}
|
|
||||||
className="asset-image"
|
|
||||||
onPointerDown={() => {
|
|
||||||
setSelectedItem({
|
|
||||||
name: asset.filename,
|
|
||||||
id: asset.AssetID,
|
|
||||||
type:
|
|
||||||
asset.type === "undefined"
|
|
||||||
? undefined
|
|
||||||
: asset.type,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="asset-name">
|
|
||||||
{asset.filename
|
|
||||||
.split("_")
|
|
||||||
.map(
|
|
||||||
(word: any) =>
|
|
||||||
word.charAt(0).toUpperCase() + word.slice(1)
|
|
||||||
)
|
|
||||||
.join(" ")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedCategory) {
|
|
||||||
return (
|
|
||||||
<div className="assets-wrapper">
|
|
||||||
<h2 className="header">
|
|
||||||
{selectedCategory}
|
|
||||||
<button
|
|
||||||
className="back-button"
|
|
||||||
id="asset-backButtom"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCategory(null);
|
|
||||||
setSelectedSubCategory(null);
|
|
||||||
setCategoryAssets([]);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="back-arrow">
|
|
||||||
<ArrowIcon />
|
|
||||||
</div>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{selectedCategory === "Decals" && (
|
|
||||||
<>
|
|
||||||
<div className="catogory-asset-filter">
|
|
||||||
{activeSubcategories.map((cat, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className={`catogory-asset-filter-wrapper ${
|
|
||||||
selectedSubCategory === cat.name ? "active" : ""
|
|
||||||
}`}
|
|
||||||
onClick={() => {
|
|
||||||
fetchCategoryDecals(cat.name);
|
|
||||||
setSelectedSubCategory(cat.name);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="sub-catagory">{cat.icon}</div>
|
|
||||||
<div className="sub-catagory">{cat.name}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{selectedCategory !== "Decals" && !selectedSubCategory ? (
|
|
||||||
<div className="assets-container">
|
|
||||||
{categoryAssets?.map((asset: any, index: number) => (
|
|
||||||
<div
|
|
||||||
key={`${index}-${asset}`}
|
|
||||||
className="assets"
|
|
||||||
id={asset.filename}
|
|
||||||
title={asset.filename}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={asset?.thumbnail}
|
|
||||||
alt={asset.filename}
|
|
||||||
className="asset-image"
|
|
||||||
onPointerDown={() => {
|
|
||||||
setSelectedItem({
|
|
||||||
name: asset.filename,
|
|
||||||
id: asset.AssetID,
|
|
||||||
type:
|
|
||||||
asset.type === "undefined"
|
|
||||||
? undefined
|
|
||||||
: asset.type,
|
|
||||||
category: asset.category,
|
|
||||||
subType: asset.subType,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="asset-name">
|
|
||||||
{asset.filename
|
|
||||||
.split("_")
|
|
||||||
.map(
|
|
||||||
(word: any) =>
|
|
||||||
word.charAt(0).toUpperCase() + word.slice(1)
|
|
||||||
)
|
|
||||||
.join(" ")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{categoryAssets.length === 0 && (
|
|
||||||
<div className="no-asset">
|
|
||||||
🚧 The asset shelf is empty. We're working on filling
|
|
||||||
it up!
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="assets-container">
|
|
||||||
{categoryAssets?.map((asset: any, index: number) => (
|
|
||||||
<div
|
|
||||||
key={`${index}-${asset}`}
|
|
||||||
className="assets"
|
|
||||||
id={asset.decalName}
|
|
||||||
title={asset.decalName}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={asset?.decalImage}
|
|
||||||
alt={asset.decalName}
|
|
||||||
className="asset-image"
|
|
||||||
onPointerDown={() => {
|
|
||||||
setDroppedDecal({
|
|
||||||
category: asset.category,
|
|
||||||
decalName: asset.decalName,
|
|
||||||
decalImage: asset.decalImage,
|
|
||||||
decalId: asset.id,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="asset-name">
|
|
||||||
{asset.decalName
|
|
||||||
.split("_")
|
|
||||||
.map(
|
|
||||||
(word: any) =>
|
|
||||||
word.charAt(0).toUpperCase() + word.slice(1)
|
|
||||||
)
|
|
||||||
.join(" ")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
{categoryAssets.length === 0 && (
|
|
||||||
<div className="no-asset">
|
|
||||||
🚧 The asset shelf is empty. We're working on filling
|
|
||||||
it up!
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="assets-wrapper">
|
|
||||||
<h2 className="categories-header">Categories</h2>
|
|
||||||
<div className="categories-container">
|
|
||||||
{Array.from(
|
|
||||||
new Set(categoryList.map((asset) => asset.category))
|
|
||||||
).map((category, index) => {
|
|
||||||
const categoryInfo = categoryList.find(
|
|
||||||
(asset) => asset.category === category
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={`${index}-${category}`}
|
|
||||||
className="category"
|
|
||||||
id={category}
|
|
||||||
onClick={() => {
|
|
||||||
fetchCategoryAssets(category);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src={categoryInfo?.categoryImage ?? ""}
|
|
||||||
alt={category}
|
|
||||||
className="category-image"
|
|
||||||
draggable={false}
|
|
||||||
/>
|
|
||||||
<div className="category-name">{category}</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Assets;
|
|
||||||
@@ -3,7 +3,7 @@ import ToggleHeader from "../../ui/inputs/ToggleHeader";
|
|||||||
import Outline from "./Outline";
|
import Outline from "./Outline";
|
||||||
import Header from "./Header";
|
import Header from "./Header";
|
||||||
import { useToggleStore } from "../../../store/useUIToggleStore";
|
import { useToggleStore } from "../../../store/useUIToggleStore";
|
||||||
import Assets from "./Assets";
|
import Assets from "./assetList/Assets";
|
||||||
import useModuleStore from "../../../store/useModuleStore";
|
import useModuleStore from "../../../store/useModuleStore";
|
||||||
import Widgets from "./visualization/widgets/Widgets";
|
import Widgets from "./visualization/widgets/Widgets";
|
||||||
import Templates from "../../../modules/visualization/template/Templates";
|
import Templates from "../../../modules/visualization/template/Templates";
|
||||||
|
|||||||
177
app/src/components/layout/sidebarLeft/assetList/Assets.tsx
Normal file
177
app/src/components/layout/sidebarLeft/assetList/Assets.tsx
Normal file
@@ -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<string | null>(null);
|
||||||
|
const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
|
||||||
|
const [assets, setAssets] = useState<AssetProp[] | DecalProp[]>([]);
|
||||||
|
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 (
|
||||||
|
<div className="assets-container-main">
|
||||||
|
<Search onChange={setSearchValue} value={searchValue} />
|
||||||
|
<div className="assets-list-section">
|
||||||
|
<section>
|
||||||
|
{isLoading ? (
|
||||||
|
<SkeletonUI type="asset" />
|
||||||
|
) : searchValue || selectedCategory ? (
|
||||||
|
<div className="assets-wrapper">
|
||||||
|
{selectedCategory ? (
|
||||||
|
<>
|
||||||
|
<h2 className="header">
|
||||||
|
{selectedCategory}
|
||||||
|
<button
|
||||||
|
className="back-button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCategory(null);
|
||||||
|
setSelectedSubCategory(null);
|
||||||
|
setAssets([]);
|
||||||
|
setSearchValue(null);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="back-arrow">
|
||||||
|
<ArrowIcon />
|
||||||
|
</div>{" "}
|
||||||
|
Back
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
{selectedCategory === "Decals" && (
|
||||||
|
<div className="catogory-asset-filter">
|
||||||
|
{ACTIVE_DECAL_SUBCATEGORIES.map((cat) => (
|
||||||
|
<div
|
||||||
|
key={cat.name}
|
||||||
|
className={`catogory-asset-filter-wrapper ${
|
||||||
|
selectedSubCategory === cat.name ? "active" : ""
|
||||||
|
}`}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
const res = await fetchCategoryDecals(cat.name);
|
||||||
|
setAssets(res);
|
||||||
|
setSelectedSubCategory(cat.name);
|
||||||
|
setIsLoading(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="sub-catagory">{cat.icon}</div>
|
||||||
|
<div className="sub-catagory">{cat.name}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="assets-container">
|
||||||
|
{filteredAssets.map((a, i) => (
|
||||||
|
<RenderAsset key={i} asset={a} index={i} />
|
||||||
|
))}
|
||||||
|
{filteredAssets.length === 0 && (
|
||||||
|
<div className="no-asset">🚧 No assets found</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<h2 className="header">Global Search Results</h2>
|
||||||
|
<div className="assets-container">
|
||||||
|
{globalResults.map((a, i) => (
|
||||||
|
<RenderAsset key={i} asset={a} index={i} />
|
||||||
|
))}
|
||||||
|
{globalResults.length === 0 && (
|
||||||
|
<div className="no-asset">🔎 No matches found</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="assets-wrapper">
|
||||||
|
<h2 className="categories-header">Categories</h2>
|
||||||
|
<div className="categories-container">
|
||||||
|
{CATEGORY_LIST.map((cat) => (
|
||||||
|
<div
|
||||||
|
key={cat.category}
|
||||||
|
className="category"
|
||||||
|
onClick={() => handleFetchCategory(cat.category)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={cat.categoryImage}
|
||||||
|
alt={cat.category}
|
||||||
|
className="category-image"
|
||||||
|
draggable={false}
|
||||||
|
/>
|
||||||
|
<div className="category-name">{cat.category}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Assets;
|
||||||
@@ -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: <AlertIcon /> },
|
||||||
|
{ name: "Navigation", icon: <NavigationIcon /> },
|
||||||
|
{ name: "Branding", icon: <HangTagIcon /> },
|
||||||
|
{ name: "Informational", icon: <DecalInfoIcon /> },
|
||||||
|
];
|
||||||
@@ -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<AssetProp[]> => {
|
||||||
|
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<AssetProp[]> => {
|
||||||
|
try {
|
||||||
|
return await fetchAssets();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch all assets", err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { getCategoryDecals } from "../../../../../services/factoryBuilder/asset/decals/getCategoryDecals";
|
||||||
|
|
||||||
|
export const fetchCategoryDecals = async (subcategory: string): Promise<DecalProp[]> => {
|
||||||
|
try {
|
||||||
|
const res = await getCategoryDecals(subcategory);
|
||||||
|
return res;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to fetch decals", err);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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 (
|
||||||
|
<div key={`${index}-${asset.decalName}`} className="assets" id={asset.decalName} title={asset.decalName}>
|
||||||
|
<img
|
||||||
|
src={asset.decalImage}
|
||||||
|
alt={asset.decalName}
|
||||||
|
className="asset-image"
|
||||||
|
onPointerDown={() =>
|
||||||
|
setDroppedDecal({
|
||||||
|
category: asset.category,
|
||||||
|
decalName: asset.decalName,
|
||||||
|
decalImage: asset.decalImage,
|
||||||
|
decalId: asset.id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="asset-name">
|
||||||
|
{asset.decalName
|
||||||
|
.split("_")
|
||||||
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||||
|
.join(" ")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={`${index}-${asset.filename}`} className="assets" id={asset.filename} title={asset.filename}>
|
||||||
|
<img
|
||||||
|
src={asset.thumbnail}
|
||||||
|
alt={asset.filename}
|
||||||
|
className="asset-image"
|
||||||
|
onPointerDown={() =>
|
||||||
|
setSelectedItem({
|
||||||
|
name: asset.filename,
|
||||||
|
id: asset.AssetID,
|
||||||
|
type: asset.type === "undefined" ? undefined : asset.type,
|
||||||
|
category: asset.category,
|
||||||
|
subType: asset.subType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="asset-name">
|
||||||
|
{asset.filename
|
||||||
|
.split("_")
|
||||||
|
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
||||||
|
.join(" ")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { ChangeEvent, useState } from "react";
|
import React, { ChangeEvent, useEffect, useState } from "react";
|
||||||
import { CloseIcon, SearchIcon } from "../../icons/ExportCommonIcons";
|
import { CloseIcon, SearchIcon } from "../../icons/ExportCommonIcons";
|
||||||
|
|
||||||
interface SearchProps {
|
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
|
placeholder?: string; // Placeholder text for the input
|
||||||
onChange: (value: string) => void; // Callback function to handle input changes
|
onChange: (value: string) => void; // Callback function to handle input changes
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,15 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
onChange(newValue); // Call the onChange prop with the new value
|
onChange(newValue); // Call the onChange prop with the new value
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value === null) {
|
||||||
|
setInputValue("");
|
||||||
|
handleBlur();
|
||||||
|
}
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
const handleClear = () => {
|
const handleClear = () => {
|
||||||
|
echo.warn("Search field cleared.");
|
||||||
setInputValue("");
|
setInputValue("");
|
||||||
onChange(""); // Clear the input value
|
onChange(""); // Clear the input value
|
||||||
};
|
};
|
||||||
@@ -48,7 +56,7 @@ const Search: React.FC<SearchProps> = ({
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="search-input"
|
className="search-input"
|
||||||
value={inputValue}
|
value={inputValue ?? ""}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ import type { CameraControls } from "@react-three/drei";
|
|||||||
const CameraShortcutsControls = () => {
|
const CameraShortcutsControls = () => {
|
||||||
const { camera, controls } = useThree();
|
const { camera, controls } = useThree();
|
||||||
|
|
||||||
|
const isTextInput = (element: Element | null): boolean =>
|
||||||
|
element instanceof HTMLInputElement ||
|
||||||
|
element instanceof HTMLTextAreaElement ||
|
||||||
|
element?.getAttribute("contenteditable") === "true";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
if (!controls) return;
|
if (!controls) return;
|
||||||
@@ -21,6 +26,8 @@ const CameraShortcutsControls = () => {
|
|||||||
|
|
||||||
const dir = new THREE.Vector3().subVectors(camera.position, target).normalize();
|
const dir = new THREE.Vector3().subVectors(camera.position, target).normalize();
|
||||||
|
|
||||||
|
if (isTextInput(document.activeElement)) return;
|
||||||
|
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case "1": // Front
|
case "1": // Front
|
||||||
pos = new THREE.Vector3(0, 0, distance).add(target);
|
pos = new THREE.Vector3(0, 0, distance).add(target);
|
||||||
|
|||||||
29
app/src/types/uiTypes.d.ts
vendored
Normal file
29
app/src/types/uiTypes.d.ts
vendored
Normal file
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user