updated asset modal

This commit is contained in:
bflatastic committed 2025-04-10 10:50:09 +07:00
1 parent 26a6a65c92
commit 94a5178bec
3 files changed
+230 -50

No files matched your search

@@ -1,9 +1,18 @@
import { TransitionDialogue } from "~/components/reusable/TransitionDialogue";
import { faPlus, faSearch, faXmark } from "@fortawesome/pro-solid-svg-icons";
import {
faPlus,
faSearch,
faChevronRight,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button, CloseButton, Input, Tooltip } from "~/components";
import { TabSelector } from "~/components/reusable/TabSelector";
import { useState } from "react";
import { useState, useEffect, useMemo } from "react";
import { ItemElements } from "../SidePanelTabs/sharedComps/ItemElements";
import { demoSkyboxItems, demoShapeItems } from "../../signals";
import { MediaItem } from "../../models";
import { useUserObjects, useFeaturedObjects } from "../SidePanelTabs/hooks";
import { FilterEngineCategories } from "~/enums";
interface AssetModalProps {
isOpen: boolean;
@@ -11,23 +20,189 @@ interface AssetModalProps {
onAddAsset: () => void;
}
type AssetTab = {
id: string;
label: string;
engineCategory?: FilterEngineCategories;
items: MediaItem[];
};
const AllTabSection = ({
label,
items,
onViewAll,
}: {
label: string;
items: MediaItem[];
onViewAll: () => void;
}) => (
<div className="mb-8">
<div className="mb-3 flex items-center justify-between">
<h3 className="text-lg font-medium opacity-90">{label}</h3>
<Button
variant="secondary"
className="flex items-center gap-1 px-2 py-1 text-sm"
onClick={onViewAll}
>
View all
<FontAwesomeIcon icon={faChevronRight} className="text-xs opacity-70" />
</Button>
</div>
<div className="h-[200px]">
<ItemElements
items={items.slice(0, 4)}
busy={false}
debug={`all-tab-section-${label}`}
/>
</div>
</div>
);
export const AssetModal = ({
isOpen,
onClose,
onAddAsset,
}: AssetModalProps) => {
const [activeTab, setActiveTab] = useState("details");
const [activeLibraryTab, setActiveLibraryTab] = useState("library");
const [activeAssetTab, setActiveAssetTab] = useState("all");
const [searchTerm, setSearchTerm] = useState("");
const tabs = [
// Fetch objects for different categories
const { userObjects: userCharacters } = useUserObjects({
filterEngineCategories: [FilterEngineCategories.CHARACTER],
defaultErrorMessage: "Error fetching user characters",
});
const { userObjects: userObjects } = useUserObjects({
filterEngineCategories: [FilterEngineCategories.OBJECT],
defaultErrorMessage: "Error fetching user objects",
});
const { userObjects: userNature } = useUserObjects({
filterEngineCategories: [FilterEngineCategories.SET_DRESSING],
defaultErrorMessage: "Error fetching user nature assets",
});
const { featuredObjects: featuredCharacters } = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.CHARACTER],
defaultErrorMessage: "Error fetching featured characters",
});
const { featuredObjects: featuredObjects } = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.OBJECT],
defaultErrorMessage: "Error fetching featured objects",
});
const { featuredObjects: featuredNature } = useFeaturedObjects({
filterEngineCategories: [FilterEngineCategories.SET_DRESSING],
defaultErrorMessage: "Error fetching featured nature assets",
});
const libraryTabs = [
{ id: "library", label: "Library" },
{ id: "mine", label: "Mine" },
];
const assetTabs: AssetTab[] = [
{ id: "all", label: "All", items: [] },
{
id: "character",
label: "Character",
engineCategory: FilterEngineCategories.CHARACTER,
items:
activeLibraryTab === "library"
? (featuredCharacters ?? [])
: (userCharacters ?? []),
},
{
id: "skybox",
label: "Skybox",
items: demoSkyboxItems.value,
},
{
id: "nature",
label: "Nature",
engineCategory: FilterEngineCategories.SET_DRESSING,
items:
activeLibraryTab === "library"
? (featuredNature ?? [])
: (userNature ?? []),
},
{
id: "objects",
label: "Objects",
engineCategory: FilterEngineCategories.OBJECT,
items:
activeLibraryTab === "library"
? [...demoShapeItems.value, ...(featuredObjects ?? [])]
: (userObjects ?? []),
},
];
// Combine all items for the "All" tab
const allItems = useMemo(() => {
return assetTabs
.filter((tab) => tab.id !== "all")
.flatMap((tab) => tab.items);
}, [assetTabs]);
// Update the items array for the "All" tab
assetTabs[0].items = allItems;
const currentTab =
assetTabs.find((tab) => tab.id === activeAssetTab) || assetTabs[0];
// Filter items based on search term
const displayedItems = useMemo(() => {
if (!searchTerm) {
return currentTab.items;
}
const searchLower = searchTerm.toLowerCase();
return currentTab.items.filter(
(item) =>
item.name?.toLowerCase().includes(searchLower) ||
item.description?.toLowerCase().includes(searchLower),
);
}, [currentTab.items, searchTerm]);
// Switch to "All" tab when searching
useEffect(() => {
if (searchTerm && activeAssetTab !== "all") {
setActiveAssetTab("all");
}
}, [searchTerm]);
const renderContent = () => {
if (activeAssetTab === "all" && !searchTerm) {
return (
<div className="space-y-2">
{assetTabs.slice(1).map((tab) => (
<AllTabSection
key={tab.id}
label={tab.label}
items={tab.items}
onViewAll={() => setActiveAssetTab(tab.id)}
/>
))}
</div>
);
}
return (
<ItemElements
items={displayedItems}
busy={false}
debug={`asset-modal-${currentTab.id}`}
/>
);
};
return (
<TransitionDialogue
isOpen={isOpen}
onClose={onClose}
className="h-[500px] max-w-3xl"
className="h-[640px] max-w-5xl"
childPadding={false}
>
<div className="grid h-full grid-cols-12 gap-3">
@@ -44,23 +219,40 @@ export const AssetModal = ({
</Tooltip>
</div>
<hr className="my-2 w-full border-white/10" />
<div className="space-y-1">{/* Asset list will go here */}</div>
<div className="space-y-1">
{assetTabs.map((tab) => (
<Button
key={tab.id}
variant={activeAssetTab === tab.id ? "primary" : "secondary"}
className="w-full justify-start text-left"
onClick={() => setActiveAssetTab(tab.id)}
>
{tab.label}
</Button>
))}
</div>
</div>
<div className="col-span-9 p-3 ps-0 pt-2">
<div className="flex h-full flex-col">
<div>
<div className="flex items-center gap-4">
<TabSelector
tabs={tabs}
activeTab={activeTab}
onTabChange={setActiveTab}
tabs={libraryTabs}
activeTab={activeLibraryTab}
onTabChange={setActiveLibraryTab}
className="w-auto"
/>
<Input placeholder="Search" className="grow" icon={faSearch} />
<Input
placeholder="Search"
className="grow"
icon={faSearch}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<CloseButton onClick={onClose} />
</div>
<div className="space-y-4">
{/* Asset details will go here */}
<div className="mt-4 h-[480px] overflow-y-auto">
{renderContent()}
</div>
</div>
<div className="mt-auto flex justify-end pt-4">
@@ -39,7 +39,7 @@ export const ItemElement = ({ item }: Props) => {
return (
<div
className="group relative w-full cursor-pointer select-none overflow-hidden rounded-lg transition-all duration-200 hover:brightness-110"
className="group relative w-full cursor-pointer select-none overflow-hidden transition-all duration-200"
onPointerDown={(event) => DndAsset.onPointerDown(event, item)}
>
{item.media_type && (
@@ -55,7 +55,7 @@ export const ItemElement = ({ item }: Props) => {
/>
)}
<div className="pointer-events-none aspect-[4.5/5] w-full select-none bg-brand-secondary-500 object-cover object-center">
<div className="pointer-events-none aspect-[16/12] w-full select-none overflow-hidden rounded-lg border-2 border-white/10 bg-brand-secondary-500 object-cover object-center transition-all group-hover:border-brand-primary">
{item.thumbnail && !imageError && (
<img
crossOrigin="anonymous"
@@ -66,7 +66,7 @@ export const ItemElement = ({ item }: Props) => {
/>
)}
</div>
<div className="pointer-events-none w-full select-none truncate bg-brand-secondary-950 px-2 py-1 text-center text-[12px] transition-all duration-200 group-hover:bg-brand-secondary-800">
<div className="pointer-events-none w-full select-none truncate py-1.5 text-start text-[13px] transition-all duration-200 ">
{item.name || item.media_id}
</div>
</div>
@@ -1,6 +1,5 @@
import { ItemElement } from "./ItemElement";
import { MediaItem } from "~/pages/PageEnigma/models";
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";
@@ -24,29 +23,12 @@ export const ItemElements = ({
onLoadMore,
hasMore,
}: Props) => {
const displayWidth =
dndSidePanelWidth.value > -1
? dndSidePanelWidth.value
: sidePanelWidth.value;
const GRID_GAP = 12;
const ASPECT_RATIO = 16 / 12;
const TEXT_HEIGHT = 32;
const COLUMN_COUNT = 4;
const GRID_GAP = 8;
const ASPECT_RATIO = 4.5 / 5;
const TEXT_HEIGHT = 24;
const columnCount = getGridColumns(displayWidth);
const rowCount = Math.ceil(items.length / columnCount);
function getGridColumns(displayWidth: number): number {
if (displayWidth <= 280) {
return 2;
} else if (displayWidth <= 360) {
return 3;
} else if (displayWidth <= 440) {
return 4;
} else {
return 4;
}
}
const rowCount = Math.ceil(items.length / COLUMN_COUNT);
const cellRenderer = ({
columnIndex,
@@ -59,14 +41,20 @@ export const ItemElements = ({
key: string;
style: React.CSSProperties;
}) => {
const index = rowIndex * columnCount + columnIndex;
const index = rowIndex * COLUMN_COUNT + columnIndex;
if (index >= items.length) return null;
// Calculate dimensions for the container
const containerWidth = (style.width as number) - GRID_GAP;
const containerHeight = (style.height as number) - GRID_GAP;
const adjustedStyle = {
...style,
padding: GRID_GAP / 2,
height: style.height as number,
boxSizing: "border-box" as const,
left: `${(style.left as number) + GRID_GAP / 2}px`,
top: `${(style.top as number) + GRID_GAP / 2}px`,
width: `${containerWidth}px`,
height: `${containerHeight}px`,
padding: 0,
};
return (
@@ -119,23 +107,23 @@ export const ItemElements = ({
<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);
// Calculate cell width accounting for gaps
const cellWidth = Math.floor(
(width - GRID_GAP * (COLUMN_COUNT + 1)) / COLUMN_COUNT,
);
const imageHeight = Math.floor(cellWidth / ASPECT_RATIO);
const cellHeight = imageHeight + TEXT_HEIGHT;
const cellHeight = imageHeight + TEXT_HEIGHT + GRID_GAP;
return (
<Grid
cellRenderer={cellRenderer}
columnCount={columnCount}
columnWidth={cellWidth}
height={height}
columnCount={COLUMN_COUNT}
columnWidth={cellWidth + GRID_GAP}
height={height - GRID_GAP}
rowCount={rowCount}
rowHeight={cellHeight}
width={width}
style={{ outline: "none" }}
columnGap={GRID_GAP}
rowGap={GRID_GAP}
overscanRowCount={2}
onScroll={handleScroll}
/>