mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-05 01:38:44 +00:00
updated asset modal
This commit is contained in:
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">
|
||||
|
||||
+3
-3
@@ -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>
|
||||
|
||||
+23
-35
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in new issue
Block a user