From 2f52ccf05c9333530d78c247ab15872749bb404f Mon Sep 17 00:00:00 2001 From: bflatastic Date: Mon, 21 Apr 2025 12:15:55 +0700 Subject: [PATCH] working upload modal --- .../app/src/components/reusable/Select.tsx | 121 +++-- .../reusable/TransitionDialogue.tsx | 16 +- .../src/components/reusable/Typography.tsx | 4 +- .../reusable/UploadModal/FileUploader.tsx | 5 +- .../reusable/UploadModal3D/UploadFiles3D.tsx | 51 +- .../reusable/UploadModal3D/UploadModal3D.tsx | 106 +++- .../utilities/loadPreviewOnCanvas.ts | 81 +-- .../PageEnigma/comps/AssetMenu/AssetModal.tsx | 493 +++++++++++------- 8 files changed, 572 insertions(+), 305 deletions(-) diff --git a/frontend/apps/editor3d/app/src/components/reusable/Select.tsx b/frontend/apps/editor3d/app/src/components/reusable/Select.tsx index f36d213a53..c3d7366296 100644 --- a/frontend/apps/editor3d/app/src/components/reusable/Select.tsx +++ b/frontend/apps/editor3d/app/src/components/reusable/Select.tsx @@ -1,76 +1,95 @@ -import { Fragment } from "react"; import { Listbox, Transition } from "@headlessui/react"; import { faCheck, faChevronDown } from "@fortawesome/pro-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { MediaFileAnimationType } from "~/enums"; +import { twMerge } from "tailwind-merge"; export type SelectValue = string | number; export type SelectOption = { label: string; value: SelectValue }; interface ListDropdownProps { options: SelectOption[]; - onChange: (val: MediaFileAnimationType) => void; + onChange: (val: SelectValue) => void; placeholder?: string; value?: SelectValue; + id?: string; } + export const Select = ({ onChange, options, placeholder, value, + id, }: ListDropdownProps) => { const selectedOption = options.find( (option: SelectOption) => option.value === value, ) || { label: placeholder || "", value: "" }; + return ( - -
- - - {selectedOption.label} - - - - - - - {options.map((option, itemIdx) => ( - - `relative cursor-pointer select-none py-2 pl-10 pr-4 text-white ${ - active ? "text-white" : "text-gray-400" - }` - } - value={option.value} +
+ + {({ open }) => ( + <> + + - {({ selected }) => ( - <> - - {option.label} - - {selected ? ( - - - ) : null} - - )} - - ))} - - -
- + {selectedOption.label} + + + + + + + + {options.map((option, itemIdx) => ( + + twMerge( + "relative cursor-pointer select-none py-2.5 pl-8 pr-2 text-white transition-all duration-150 ease-in-out", + active && "bg-brand-primary/20 text-white", + selected && "bg-brand-primary/40", + !selected && "text-white/90", + ) + } + value={option.value} + > + {({ selected }) => ( + <> + + {option.label} + + {selected ? ( + + + ) : null} + + )} + + ))} + + + + )} + +
); }; diff --git a/frontend/apps/editor3d/app/src/components/reusable/TransitionDialogue.tsx b/frontend/apps/editor3d/app/src/components/reusable/TransitionDialogue.tsx index a41b061dd9..810fa016ed 100644 --- a/frontend/apps/editor3d/app/src/components/reusable/TransitionDialogue.tsx +++ b/frontend/apps/editor3d/app/src/components/reusable/TransitionDialogue.tsx @@ -27,11 +27,17 @@ const DialogBackdrop = ({ className }: { className?: string }) => { enter="ease-out duration-300" enterFrom="opacity-0" enterTo="opacity-100" - leave="ease-in duration-200" + leave="ease-in duration-100" leaveFrom="opacity-100" leaveTo="opacity-0" > -
+
{ + e.preventDefault(); + e.stopPropagation(); + }} + /> ); }; @@ -71,7 +77,7 @@ export const TransitionDialogue = ({
{ + e.preventDefault(); + e.stopPropagation(); + }} > {title && ( ( export const Label = ({ className, children, + required, ...rest -}: LabelHTMLAttributes) => ( +}: LabelHTMLAttributes & { required?: boolean }) => ( ); diff --git a/frontend/apps/editor3d/app/src/components/reusable/UploadModal/FileUploader.tsx b/frontend/apps/editor3d/app/src/components/reusable/UploadModal/FileUploader.tsx index 003bcd36d4..6032a462c2 100644 --- a/frontend/apps/editor3d/app/src/components/reusable/UploadModal/FileUploader.tsx +++ b/frontend/apps/editor3d/app/src/components/reusable/UploadModal/FileUploader.tsx @@ -1,5 +1,6 @@ import { FileUploader as DragDropFilesUploader } from "react-drag-drop-files"; import { DragAndDropZone } from "./DragAndDropZone"; +import { Label } from "../Typography"; interface Props { title: string; @@ -10,8 +11,8 @@ interface Props { export const FileUploader = ({ fileTypes, title, file, setFile }: Props) => { return ( -
- {title} +
+ { setFile(file); diff --git a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadFiles3D.tsx b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadFiles3D.tsx index 17a70e7330..eebe30d3ed 100644 --- a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadFiles3D.tsx +++ b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadFiles3D.tsx @@ -1,11 +1,12 @@ import { useCallback, useEffect, useState, useRef } from "react"; - -import { Button, H6, Input, ListDropdown } from "~/components"; +import { Button, H6, Input, Label, ListDropdown } from "~/components"; import { FileUploader } from "../UploadModal/FileUploader"; import { loadPreviewOnCanvas, snapshotCanvasAsThumbnail } from "./utilities"; import { upload3DObjects } from "./utilities/upload3DObjects"; import { UploaderState } from "~/models"; import { FilterEngineCategories, MediaFileAnimationType } from "~/enums"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faCube } from "@fortawesome/pro-solid-svg-icons"; interface Props { title: string; @@ -21,7 +22,6 @@ interface Props { } export const UploadFiles3D = ({ - title, fileTypes, engineCategory, options, @@ -121,16 +121,19 @@ export const UploadFiles3D = ({ return ( <>
- setUploadTitle({ value: event.target.value })} - className={uploadTitle.error ? "mb-3" : ""} - /> +
+ + setUploadTitle({ value: event.target.value })} + className={uploadTitle.error ? "mb-3" : ""} + /> +
{ @@ -149,16 +152,20 @@ export const UploadFiles3D = ({
{assetFile.error}
)} -
- +
+ {!assetFile.value && ( -
- File Preview +
+ + Your model preview will appear here
)} {previewStatus.type.includes("Error") && ( <> -
+
{previewStatus.type} {previewStatus.message &&
} {previewStatus.message} @@ -167,13 +174,17 @@ export const UploadFiles3D = ({ )}
-
- +
+
diff --git a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx index e7fd87d17f..486904c536 100644 --- a/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx +++ b/frontend/apps/editor3d/app/src/components/reusable/UploadModal3D/UploadModal3D.tsx @@ -1,20 +1,30 @@ import { useEffect, useState } from "react"; -import { TransitionDialogue, LoadingDots } from "~/components"; +import { + TransitionDialogue, + LoadingDots, + Label, + CloseButton, +} from "~/components"; import { UploadAssetError } from "../UploadModal/UploadAssetError"; import { UploadSuccess } from "../UploadModal/UploadSuccess"; import { UploadFiles3D } from "./UploadFiles3D"; -import { FilterEngineCategories, UploaderStates } from "~/enums"; import { initialUploaderState, UploaderState } from "~/models"; +import { Select } from "../Select"; +import { + FilterEngineCategories, + UploaderStates, + OBJECT_FILE_TYPE, + CHARACTER_MIXAMO_FILE_TYPE, + IMAGEPLANE_FILE_TYPE, +} from "~/enums"; interface Props { onClose: () => void; onSuccess: () => void; isOpen: boolean; title: string; - fileTypes: string[]; - engineCategory: FilterEngineCategories; options?: { fileSubtypes?: { [key: string]: string }[]; hasLength?: boolean; @@ -22,17 +32,52 @@ interface Props { }; } +// Map categories to their allowed file types +const categoryFileTypes: Partial> = { + [FilterEngineCategories.OBJECT]: Object.values(OBJECT_FILE_TYPE), + [FilterEngineCategories.CHARACTER]: [ + ...Object.values(CHARACTER_MIXAMO_FILE_TYPE), + ], + [FilterEngineCategories.IMAGE_PLANE]: Object.values(IMAGEPLANE_FILE_TYPE), + [FilterEngineCategories.LOCATION]: Object.values(OBJECT_FILE_TYPE), + [FilterEngineCategories.CREATURE]: Object.values(OBJECT_FILE_TYPE), +}; + +// Excluded category options for the upload modal +const categoryOptions = Object.entries(FilterEngineCategories) + .filter( + ([, value]) => + ![ + "audio", + "video_plane", + "skybox", + "set_dressing", + "expression", + "animation", + "scene", + ].includes(value), + ) + .sort(([, a], [, b]) => (a === "object" ? -1 : b === "object" ? 1 : 0)) + .map(([key, value]) => ({ + // This is because sets is location i guess - BFlat + label: + key === "LOCATION" + ? "Set" + : key.charAt(0) + key.slice(1).toLowerCase().replace(/_/g, " "), + value: value, + })); + export function UploadModal3D({ isOpen, onClose, onSuccess, title, - fileTypes, - engineCategory, options, }: Props) { const [uploaderState, setUploaderState] = useState(initialUploaderState); + const [selectedCategory, setSelectedCategory] = + useState(FilterEngineCategories.OBJECT); const updateUploaderState = (newState: UploaderState) => { setUploaderState(newState); @@ -52,14 +97,35 @@ export function UploadModal3D({ switch (uploaderState.status) { case UploaderStates.ready: return ( - +
+
+ + setReopenAfterAdd(e.target.checked)} - className="h-4 w-4 rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary" - /> - -
-
-
-
-
-
- -
- setSearchTerm(e.target.value)} - iconClassName="text-white/60" - /> - {searchTerm && ( - + <> + +
+
+
+

+ 3D Assets +

+ + + +
+
+
+ {assetTabs.map((tab) => ( +
-
setActiveAssetTab(tab.id)} + > + + {tab.label} + + ))} +
+
+ setReopenAfterAdd(e.target.checked)} + className="h-4 w-4 rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary" + /> +
+
+
+
+
+ +
+ setSearchTerm(e.target.value)} + iconClassName="text-white/60" + /> + {searchTerm && ( + - - {currentTab.label} + )}
- )} - {renderContent()} + +
+
+ {activeAssetTab !== "all" && !searchTerm && ( +
+ + {currentTab.label} +
+ )} + {renderContent()} +
-
-
+ + setIsUploadModalOpen(false)} + onSuccess={handleUploadSuccess} + isOpen={isUploadModalOpen} + title="Upload 3D Asset" + /> + ); };