Compare commits

...
Author SHA1 Message Date
bflatastic 2b176d023e updated upload flow - auto open asset modal after upload 2025-05-07 16:04:30 +07:00
bflatastic eef929c065 improved upload 2025-05-07 14:23:45 +07:00
bflatastic 91ea263b0c tab selector to have 3d first, and logic to hide tooltip when asset modal open 2025-05-07 12:42:04 +07:00
Brandon Thomas 196e1c4615 fix broken package (#585) 2025-05-06 17:54:34 -04:00
bflatastic 8f321f06d0 thumnail to always load in img tag even if there's error 2025-05-06 22:30:27 +07:00
bflatastic 0b338f3886 login logic fixed 2025-05-06 14:26:49 +07:00
BFlat de76fc6c36 login modal on 3D (#584)
- login modal is a shared component that
- finished but needs checking session if openai/sora logged in or not
- replaced video for 3d (it had a black screen at the end)
2025-05-06 13:15:05 +07:00
bflatastic 9fb0ca2dab fixed logo being squished 2025-05-06 10:36:09 +07:00
29 changed files with 730 additions and 156 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

@@ -1,12 +1,7 @@
import { useEffect, useState } from "react";
import {
TransitionDialogue,
LoadingDots,
Label,
CloseButton,
} from "~/components";
import { LoadingDots } from "~/components";
import { Modal } from "@storyteller/ui-modal";
import { Label } from "@storyteller/ui-label";
import { UploadAssetError } from "../UploadModal/UploadAssetError";
import { UploadSuccess } from "../UploadModal/UploadSuccess";
import { UploadFiles3D } from "./UploadFiles3D";
@@ -19,18 +14,21 @@ import {
CHARACTER_MIXAMO_FILE_TYPE,
IMAGEPLANE_FILE_TYPE,
} from "~/enums";
import { assetModalVisible } from "~/pages/PageEnigma/signals";
import { IconDefinition } from "@fortawesome/fontawesome-svg-core";
interface Props {
onClose: () => void;
onSuccess: () => void;
onSuccess: (category: FilterEngineCategories) => void;
isOpen: boolean;
title: string;
titleIcon: IconDefinition;
options?: {
fileSubtypes?: { [key: string]: string }[];
hasLength?: boolean;
hasThumbnailUpload?: boolean;
};
preselectedCategory?: FilterEngineCategories;
isSelectVisible: boolean;
}
// Map categories to their allowed file types
@@ -43,16 +41,6 @@ const categoryFileTypes: Partial<Record<FilterEngineCategories, string[]>> = {
[FilterEngineCategories.LOCATION]: Object.values(OBJECT_FILE_TYPE),
[FilterEngineCategories.CREATURE]: Object.values(OBJECT_FILE_TYPE),
};
// Map engine categories to asset modal tabs
const categoryToAssetTab: Partial<Record<FilterEngineCategories, string>> = {
[FilterEngineCategories.OBJECT]: "objects",
[FilterEngineCategories.CHARACTER]: "character",
[FilterEngineCategories.IMAGE_PLANE]: "image-planes",
[FilterEngineCategories.LOCATION]: "sets",
[FilterEngineCategories.CREATURE]: "creatures",
};
// Excluded category options for the upload modal
const categoryOptions = Object.entries(FilterEngineCategories)
.filter(
@@ -82,12 +70,19 @@ export function UploadModal3D({
onClose,
onSuccess,
title,
titleIcon,
options,
}: Props) {
preselectedCategory,
isSelectVisible,
}: Props & { preselectedCategory?: FilterEngineCategories }) {
const [uploaderState, setUploaderState] =
useState<UploaderState>(initialUploaderState);
const [selectedCategory, setSelectedCategory] =
useState<FilterEngineCategories>(FilterEngineCategories.OBJECT);
useState<FilterEngineCategories>(
preselectedCategory !== undefined
? preselectedCategory
: FilterEngineCategories.OBJECT,
);
const updateUploaderState = (newState: UploaderState) => {
setUploaderState(newState);
@@ -103,30 +98,51 @@ export function UploadModal3D({
}
}, [isOpen]);
useEffect(() => {
if (preselectedCategory !== undefined) {
setSelectedCategory(preselectedCategory);
}
}, [preselectedCategory]);
useEffect(() => {
console.log("Preselected category:", preselectedCategory);
console.log("Initial selected category:", selectedCategory);
}, [preselectedCategory, selectedCategory]);
useEffect(() => {
if (uploaderState.status === UploaderStates.success) {
onSuccess(selectedCategory);
onClose();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [uploaderState.status]);
const UploaderModalContent = () => {
switch (uploaderState.status) {
case UploaderStates.ready:
return (
<div className="space-y-4">
<div className="flex w-full flex-col">
<Label>Category</Label>
<Select
id="category-select"
options={categoryOptions}
value={selectedCategory}
onChange={(value) => {
if (
typeof value === "string" &&
Object.values(FilterEngineCategories).includes(
value as FilterEngineCategories,
)
) {
setSelectedCategory(value as FilterEngineCategories);
}
}}
placeholder="Select a category"
/>
</div>
{isSelectVisible && (
<div className="flex w-full flex-col">
<Label className="mb-1">Category</Label>
<Select
id="category-select"
options={categoryOptions}
value={selectedCategory}
onChange={(value) => {
if (
typeof value === "string" &&
Object.values(FilterEngineCategories).includes(
value as FilterEngineCategories,
)
) {
setSelectedCategory(value as FilterEngineCategories);
}
}}
placeholder="Select a category"
/>
</div>
)}
<UploadFiles3D
title={title}
engineCategory={selectedCategory}
@@ -146,31 +162,17 @@ export function UploadModal3D({
<div className="w-100 text-center opacity-50">Uploading...</div>
</>
);
case UploaderStates.success:
case UploaderStates.success: {
return (
<UploadSuccess
title="3D model"
onOk={() => {
onSuccess();
onClose();
// Store the selected category and tab in sessionStorage
const assetTab = categoryToAssetTab[selectedCategory];
if (assetTab) {
sessionStorage.setItem(
"lastUploadedCategory",
selectedCategory,
);
sessionStorage.setItem("lastUploadedTab", assetTab);
// Reopen the asset modal
assetModalVisible.value = true;
} else {
// Clear any existing stored values if we don't have a valid tab
sessionStorage.removeItem("lastUploadedCategory");
sessionStorage.removeItem("lastUploadedTab");
}
onSuccess(selectedCategory);
}}
/>
);
}
case UploaderStates.assetError:
return (
<UploadAssetError
@@ -199,15 +201,15 @@ export function UploadModal3D({
};
return (
<TransitionDialogue
<Modal
isOpen={isOpen}
onClose={onClose}
titleIcon={titleIcon}
title={title}
className="max-w-xl"
showClose={false}
showClose={true}
>
<CloseButton className="absolute right-4 top-4" onClick={onClose} />
<UploaderModalContent />
</TransitionDialogue>
</Modal>
);
}
@@ -1,7 +1,6 @@
import { useState } from "react";
import {
faChevronLeft,
faChevronRight,
faGear,
faImages,
} from "@fortawesome/pro-solid-svg-icons";
@@ -30,8 +29,8 @@ interface Props {
}
const appTabs: TabItem[] = [
{ id: "2D", label: "2D" },
{ id: "3D", label: "3D" },
{ id: "2D", label: "2D" },
];
export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
@@ -45,10 +44,6 @@ export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
const [activeLibraryTab, setActiveLibraryTab] = useState("my-media");
const [isSettingsModalOpen, setIsSettingsModalOpen] = useState(false);
const handleClick = () => {
window.location.href = "https://storyteller-2d.netlify.app/";
};
return (
<>
<header className="fixed left-0 top-0 z-[60] w-full border-b border-white/5 bg-ui-background">
@@ -74,7 +69,8 @@ export const TopBar = ({ pageName, appTabIdSignal, setAppTabId }: Props) => {
tabs={appTabs}
activeTab={appTabIdSignal.value}
disabled={false}
onTabChange={(tabId) => setAppTabId(tabId)}
onTabChange={(tabId: string) => setAppTabId(tabId)}
className="w-fit"
/>
{/* <Button
variant="secondary"
@@ -1,5 +1,4 @@
import React, { useEffect } from "react";
import { invoke } from "@tauri-apps/api/core";
import React, { useEffect, useState } from "react";
import { useSignals } from "@preact/signals-react/runtime";
import { LoadingDots, TopBar } from "~/components";
import { Controls3D } from "./comps/Controls3D";
@@ -23,10 +22,9 @@ import { Outliner } from "./comps/Outliner";
import { CameraAspectRatio } from "./enums";
import { PromptBox } from "./comps/PromptBox";
import { OnboardingHelper } from "./comps/OnboardingHelper";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay";
import { DemoModal } from "@storyteller/ui-demo-modal";
import { appTabId, setAppTabId } from "~/signals/appTab";
import { LoginModal } from "@storyteller/ui-login-modal";
export const PageEditor = () => {
useSignals();
@@ -158,7 +156,7 @@ export const PageEditor = () => {
<Timeline />
</div> */}
<DemoModal
{/* <DemoModal
title="Welcome to ArtCraft 3D"
subTitle="Your 3D editor for digital art and design"
description="Set up your scene by adding objects and start bringing your ideas to life!"
@@ -171,7 +169,14 @@ export const PageEditor = () => {
console.error("This is not the Desktop app.");
}
}}
/>
/> */}
{/* <LoginModal
onClose={() => {}}
videoSrc2D="/resources/videos/artcraft-canvas-demo.mp4"
videoSrc3D="/resources/videos/artcraft-3d-demo.mp4"
openAiLogo="/resources/images/openai-logo.png"
/> */}
</div>
);
};
@@ -1,7 +1,6 @@
import { Modal } from "@storyteller/ui-modal";
import { UploadModal3D } from "~/components/reusable/UploadModal3D";
import {
faPlus,
faSearch,
faChevronLeft,
faLayerGroup,
@@ -12,13 +11,13 @@ import {
faMountainCity,
faDog,
faImage,
faUpFromLine,
} from "@fortawesome/pro-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Tooltip } from "@storyteller/ui-tooltip";
import { Button } from "@storyteller/ui-button";
import { CloseButton } from "@storyteller/ui-close-button";
import { Input } from "@storyteller/ui-input";
import { TabSelector } from "~/components/reusable/TabSelector";
import { TabSelector } from "@storyteller/ui-tab-selector";
import React, {
useState,
useEffect,
@@ -45,6 +44,7 @@ import { isAnyStatusFetching } from "../SidePanelTabs/utilities";
type AssetTab = {
id: string;
label: string;
labelSingle?: string;
icon: typeof faLayerGroup;
engineCategory?: FilterEngineCategories;
items: MediaItem[];
@@ -81,6 +81,21 @@ const AllTabSection = ({
</div>
);
const categoryToTabIdMap: Record<FilterEngineCategories, string> = {
[FilterEngineCategories.CHARACTER]: "characters",
[FilterEngineCategories.CREATURE]: "creatures",
[FilterEngineCategories.IMAGE_PLANE]: "image-planes",
[FilterEngineCategories.LOCATION]: "sets",
[FilterEngineCategories.OBJECT]: "objects",
[FilterEngineCategories.SKYBOX]: "skybox",
[FilterEngineCategories.ANIMATION]: "all",
[FilterEngineCategories.AUDIO]: "all",
[FilterEngineCategories.EXPRESSION]: "all",
[FilterEngineCategories.SCENE]: "all",
[FilterEngineCategories.SET_DRESSING]: "all",
[FilterEngineCategories.VIDEO_PLANE]: "all",
};
export const AssetModal = () => {
useSignals();
const [activeLibraryTab, setActiveLibraryTab] = useState("library");
@@ -88,6 +103,9 @@ export const AssetModal = () => {
const [searchTerm, setSearchTerm] = useState("");
const [isUploadModalOpen, setIsUploadModalOpen] = useState(false);
const searchInputRef = useRef<HTMLInputElement>(null);
const [selectedCategory, setSelectedCategory] =
useState<FilterEngineCategories | null>(null);
const [isSelectVisible, setIsSelectVisible] = useState(true);
const handleReopenChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.checked;
@@ -101,6 +119,10 @@ export const AssetModal = () => {
assetModalVisible.value = false;
};
const handleOpen = () => {
assetModalVisible.value = true;
};
useEffect(() => {
if (assetModalVisible.value) {
// Check for stored category and tab from upload
@@ -116,7 +138,10 @@ export const AssetModal = () => {
const timer = setTimeout(() => {
searchInputRef.current?.focus();
}, 100);
return () => clearTimeout(timer);
return () => {
clearTimeout(timer);
// Ensure no side effects remain
};
}
return undefined;
// eslint-disable-next-line react-hooks/exhaustive-deps
@@ -232,7 +257,8 @@ export const AssetModal = () => {
{ id: "all", label: "All", icon: faLayerGroup, items: [] },
{
id: "character",
label: "Character",
label: "Characters",
labelSingle: "Character",
icon: faUser,
engineCategory: FilterEngineCategories.CHARACTER,
items:
@@ -243,6 +269,7 @@ export const AssetModal = () => {
{
id: "objects",
label: "Objects",
labelSingle: "Object",
icon: faCube,
engineCategory: FilterEngineCategories.OBJECT,
items:
@@ -253,6 +280,7 @@ export const AssetModal = () => {
{
id: "sets",
label: "Sets",
labelSingle: "Set",
icon: faMountainCity,
engineCategory: FilterEngineCategories.LOCATION,
items:
@@ -263,6 +291,7 @@ export const AssetModal = () => {
{
id: "creatures",
label: "Creatures",
labelSingle: "Creature",
icon: faDog,
engineCategory: FilterEngineCategories.CREATURE,
items:
@@ -273,12 +302,14 @@ export const AssetModal = () => {
{
id: "skybox",
label: "Skybox",
labelSingle: "Skybox",
icon: faSun,
items: activeLibraryTab === "library" ? demoSkyboxItems.value : [],
},
{
id: "image-planes",
label: "Image Planes",
labelSingle: "Image Plane",
icon: faImage,
engineCategory: FilterEngineCategories.IMAGE_PLANE,
items:
@@ -397,11 +428,20 @@ export const AssetModal = () => {
};
const handleAddAsset = () => {
assetModalVisible.value = false;
handleClose();
setIsUploadModalOpen(true);
setSelectedCategory(null);
setIsSelectVisible(true);
};
const handleUploadSuccess = () => {
const handleAddSpecificAsset = (category: FilterEngineCategories) => {
handleClose();
setIsUploadModalOpen(true);
setSelectedCategory(category);
setIsSelectVisible(false);
};
const handleUploadSuccess = (category: FilterEngineCategories) => {
if (reopenAfterDragSignal.value) {
// Small delay to allow the modal to close and reopen
setTimeout(() => {
@@ -410,10 +450,32 @@ export const AssetModal = () => {
} else {
handleClose();
}
// Use the provided category to find the AssetTab ID
const lastUploadedTabId = categoryToTabIdMap[category] || "all";
// Close the upload modal
setTimeout(() => {
sessionStorage.setItem("lastUploadedTab", lastUploadedTabId);
console.log("AssetModal: lastUploadedTabId set to:", lastUploadedTabId);
setIsUploadModalOpen(false);
handleOpen();
}, 300);
fetchUserCharacters();
fetchUserObjects();
fetchUserSets();
fetchUserCreatures();
fetchUserImagePlanes();
};
const clearSearch = () => setSearchTerm("");
const getArticle = (word: string | undefined) => {
if (!word) return "a";
return /^[aeiou]/i.test(word) ? "an" : "a";
};
return (
<>
<Modal
@@ -429,17 +491,31 @@ export const AssetModal = () => {
<h2 className="text-[18px] font-semibold opacity-80">
3D Assets
</h2>
<Tooltip content="Upload model" position="top" delay={200}>
{/* <Tooltip content="Upload model" position="top" delay={200}>
<Button
className="h-6 w-6 rounded-full border-none bg-transparent text-white/70 transition-colors hover:bg-transparent hover:text-white/100"
className="flex h-7 w-7 items-center justify-center rounded-md border-none bg-primary/50 text-white/70 transition-colors hover:bg-primary/70 hover:text-white/100"
onClick={handleAddAsset}
>
<FontAwesomeIcon icon={faPlus} className="text-xl" />
<FontAwesomeIcon icon={faPlus} className="text-lg" />
</Button>
</Tooltip>
</Tooltip> */}
</div>
<hr className="my-2 w-full border-white/10" />
<div className="space-y-2">
<div className="flex h-full flex-col space-y-2">
<Button
variant="primary"
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
"border-primary bg-primary/70 hover:bg-primary",
)}
onClick={handleAddAsset}
>
<FontAwesomeIcon
icon={faUpFromLine}
className="mr-2 text-lg opacity-70"
/>
Upload an asset
</Button>
{assetTabs.map((tab) => (
<Button
key={tab.id}
@@ -447,7 +523,7 @@ export const AssetModal = () => {
className={twMerge(
"w-full justify-start rounded-xl border border-white/[2%] bg-white/[4%] px-3.5 py-2.5 text-left hover:bg-white/15",
activeAssetTab === tab.id &&
"border-brand-primary bg-brand-primary/10 hover:bg-brand-primary/10",
"border-primary bg-primary/10 hover:bg-primary/10",
)}
onClick={() => setActiveAssetTab(tab.id)}
>
@@ -479,7 +555,7 @@ export const AssetModal = () => {
id="reopen-after-add"
checked={reopenAfterDragSignal.value}
onChange={handleReopenChange}
className="h-4 w-4 cursor-pointer rounded-lg border-gray-300 bg-gray-700 text-brand-primary focus:ring-brand-primary"
className="h-4 w-4 cursor-pointer rounded-lg border-gray-300 bg-gray-700 text-primary focus:ring-primary"
/>
<label
htmlFor="reopen-after-add"
@@ -529,18 +605,36 @@ export const AssetModal = () => {
)}
>
{activeAssetTab !== "all" && !searchTerm && (
<div className="mb-2 flex items-center font-semibold">
<Button
variant="secondary"
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
onClick={() => setActiveAssetTab("all")}
>
<FontAwesomeIcon
icon={faChevronLeft}
className="text-sm font-semibold opacity-70"
/>
</Button>
{currentTab.label}
<div className="mb-2 flex items-center justify-between font-semibold">
<div className="flex items-center">
<Button
variant="secondary"
className="flex items-center gap-2 border-none bg-transparent px-3 py-1.5 text-sm text-white/70 hover:bg-transparent hover:text-white/100"
onClick={() => setActiveAssetTab("all")}
>
<FontAwesomeIcon
icon={faChevronLeft}
className="text-sm font-semibold opacity-70"
/>
</Button>
{currentTab.label}
</div>
{activeAssetTab !== "skybox" &&
activeAssetTab !== "all" && (
<Button
icon={faUpFromLine}
onClick={() =>
handleAddSpecificAsset(
currentTab.engineCategory ||
FilterEngineCategories.OBJECT,
)
}
className="border-primary bg-primary/70 px-2 py-1 text-xs transition-colors hover:bg-primary"
>
Upload {getArticle(currentTab.labelSingle)}{" "}
{currentTab.labelSingle}
</Button>
)}
</div>
)}
{renderContent()}
@@ -551,10 +645,15 @@ export const AssetModal = () => {
</div>
</Modal>
<UploadModal3D
onClose={() => setIsUploadModalOpen(false)}
onSuccess={handleUploadSuccess}
onClose={() => {
setIsUploadModalOpen(false);
}}
onSuccess={(category) => handleUploadSuccess(category)}
isOpen={isUploadModalOpen}
title="Upload 3D Asset"
title={`Upload ${selectedCategory ? (selectedCategory.toLowerCase().replace(/_/g, " ") === "location" ? "sets" : selectedCategory.toLowerCase().replace(/_/g, " ")) : "3D Asset"}`}
titleIcon={!isSelectVisible ? currentTab.icon : faUpFromLine}
preselectedCategory={selectedCategory || FilterEngineCategories.OBJECT}
isSelectVisible={isSelectVisible}
/>
</>
);
@@ -23,7 +23,8 @@ export const Controls3D = () => {
useEffect(() => {
// Check if scene is empty and onboarding helper is not visible
const checkSceneEmpty = () => {
const isSceneEmpty = outlinerState.items.value.length === 0;
const isSceneEmpty =
outlinerState.items.value.length === 0 && !assetModalVisible.value;
setShowEmptySceneTooltip(isSceneEmpty);
};
@@ -37,7 +38,8 @@ export const Controls3D = () => {
return () => {
unsubscribe();
};
}, []);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [assetModalVisible.value]);
const handleModeChange = (value: string) => {
selectedMode.value = value;
@@ -119,7 +121,7 @@ export const Controls3D = () => {
<Tooltip
content="Add 3D asset to scene (B)"
position="bottom"
delay={100}
delay={300}
closeOnClick
className={twMerge(
showEmptySceneTooltip ? "hidden" : "block",
@@ -3,7 +3,7 @@ import { useSignals } from "@preact/signals-react/runtime";
import DndAsset from "~/pages/PageEnigma/DragAndDrop/DndAsset";
import { Badge } from "~/components";
import { AssetType } from "~/enums";
import { useState } from "react";
// import { useState } from "react";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUpDownLeftRight } from "@fortawesome/pro-solid-svg-icons";
@@ -37,7 +37,7 @@ const patchExpressionObjectType = (mediaType: string) => {
export const ItemElement = ({ item }: Props) => {
useSignals();
const [imageError, setImageError] = useState(false);
// const [imageError, setImageError] = useState(false);
return (
<div className="group relative w-full select-none overflow-hidden transition-all duration-200">
@@ -59,16 +59,13 @@ export const ItemElement = ({ item }: Props) => {
onPointerDown={(event) => DndAsset.onPointerDown(event, item)}
style={{ cursor: "grab", pointerEvents: "auto" }}
>
{item.thumbnail && !imageError && (
<img
crossOrigin="anonymous"
referrerPolicy="no-referrer"
src={item.thumbnail}
alt={item.name}
className="h-full w-full object-cover object-center"
onError={() => setImageError(true)}
/>
)}
<img
crossOrigin="anonymous"
referrerPolicy="no-referrer"
src={item.thumbnail}
alt={item.name}
className="h-full w-full object-cover object-center"
/>
<div className="text-shadow-md absolute inset-0 flex items-center justify-center bg-brand-primary-950/50 text-[13px] font-medium text-white opacity-0 transition-opacity duration-200 group-hover:opacity-100">
<FontAwesomeIcon icon={faUpDownLeftRight} className="mr-1.5" />
+9 -6
View File
@@ -54,6 +54,15 @@
{
"path": "../../libs/components/demo-modal"
},
{
"path": "../../libs/soundboard"
},
{
"path": "../../libs/components/tab-selector"
},
{
"path": "../../libs/api"
},
{
"path": "../../libs/components/tooltip"
},
@@ -65,12 +74,6 @@
},
{
"path": "../../libs/tauri-utils"
},
{
"path": "../../libs/api"
},
{
"path": "../../libs/soundboard"
}
]
}
@@ -54,7 +54,6 @@ export function DemoModal({
<video autoPlay muted loop controls={false}>
<source src={videoSrc} type="video/mp4" />
</video>
W
</div>
<Button
className="font-semibold"
@@ -0,0 +1,12 @@
{
"presets": [
[
"@nx/react/babel",
{
"runtime": "automatic",
"useBuiltIns": "usage"
}
]
],
"plugins": []
}
@@ -0,0 +1,7 @@
# @storyteller/ui-login-modal
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test @storyteller/ui-login-modal` to execute the unit tests via [Vitest](https://vitest.dev/).
@@ -0,0 +1,17 @@
{
"name": "@storyteller/ui-login-modal",
"version": "0.0.1",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"development": "./src/index.ts",
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"default": "./dist/index.js"
}
}
}
@@ -0,0 +1,2 @@
export * from './lib/login-modal';
@@ -0,0 +1,7 @@
/*
* Replace this with your own classes
*
* e.g.
* .container {
* }
*/
@@ -0,0 +1,12 @@
import { render } from '@testing-library/react';
import LoginModal from './login-modal';
describe('LoginModal', () => {
it('should render successfully', () => {
const { baseElement } = render(<LoginModal />);
expect(baseElement).toBeTruthy();
});
});
@@ -0,0 +1,273 @@
import { Button } from "@storyteller/ui-button";
import { Transition, TransitionChild } from "@headlessui/react";
import { createPortal } from "react-dom";
import { useState, useEffect } from "react";
import {
faArrowRight,
faRightToBracket,
} from "@fortawesome/pro-solid-svg-icons";
import { faDiscord } from "@fortawesome/free-brands-svg-icons";
import { IsDesktopApp } from "@storyteller/tauri-utils";
import { invoke } from "@tauri-apps/api/core";
interface LoginModalProps {
onClose: () => void;
videoSrc2D: string;
videoSrc3D: string;
openAiLogo: string;
}
export function LoginModal({
onClose,
videoSrc2D,
videoSrc3D,
openAiLogo,
}: LoginModalProps) {
const [step, setStep] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const [hasSession, setHasSession] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const totalSteps = 3;
const checkSession = async () => {
// Return the current session state
return hasSession;
};
// Check session on component mount
useEffect(() => {
const initSession = async () => {
const sessionExists = await checkSession();
if (!sessionExists) {
setIsOpen(true);
setStep(1);
}
};
initSession();
}, []);
const handleClose = () => {
setIsOpen(false);
onClose();
};
const handleNext = async () => {
if (step === 2) {
setIsLoading(true);
try {
if (IsDesktopApp()) {
await invoke("open_sora_login_command");
// Add 3 second delay before setting session to true
await new Promise((resolve) => setTimeout(resolve, 3000));
setHasSession(true);
const sessionExists = await checkSession();
if (sessionExists) {
setStep(step + 1);
}
} else {
alert("Please open the desktop app to login");
// Add 3 second delay before setting session to true
await new Promise((resolve) => setTimeout(resolve, 3000));
setHasSession(true);
const sessionExists = true;
if (sessionExists) {
setStep(step + 1);
}
}
} catch (error) {
console.error("Login failed:", error);
} finally {
setIsLoading(false);
}
} else if (step < totalSteps) {
setStep(step + 1);
}
};
const handleBack = () => {
if (step > 1) setStep(step - 1);
};
// Progress bar rendering
const renderProgress = () => (
<div className="flex items-center justify-center gap-2 mb-2">
{[...Array(totalSteps)].map((_, idx) => (
<div
key={idx}
className={`h-1.5 rounded transition-all duration-300 w-16 ${
idx < step ? "bg-primary" : "bg-white/30"
}`}
/>
))}
</div>
);
// Step content rendering
const renderStepContent = () => {
switch (step) {
case 1:
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Welcome to ArtCraft
</h2>
<p className="text-white/70 mb-6 text-center">
Here's what you can do...
</p>
<div className="grid grid-cols-2 gap-6 h-full grow">
<div className="h-full">
<div className="aspect-[4/3] w-full overflow-hidden bg-black/20 rounded-t-lg">
<video
className="object-cover w-full h-full"
autoPlay
muted
loop
controls={false}
>
<source src={videoSrc2D} type="video/mp4" />
</video>
</div>
<p className="text-center px-1.5 py-2 text-white/90 bg-black/20 rounded-b-lg font-medium text-sm">
2D Canvas
</p>
</div>
<div>
<div className="aspect-[4/3] w-full overflow-hidden bg-black/20 rounded-t-lg">
<video
className="object-cover w-full h-full"
autoPlay
muted
loop
controls={false}
>
<source src={videoSrc3D} type="video/mp4" />
</video>
</div>
<p className="text-center px-1.5 py-2 text-white/90 bg-black/20 rounded-b-lg font-medium text-sm">
3D Scene Editor
</p>
</div>
</div>
</div>
);
case 2:
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Login with your OpenAI account
</h2>
<p className="text-white/70 mb-6 text-center px-16">
We're the application layer for images and video. To provide you
with <b>100% free service</b>, we use your existing AI accounts.
To start off, you'll need to add your OpenAI Sora account. If you
have a ChatGPT subscription, you can log in with Sora.
</p>
<img
src={openAiLogo}
alt="OpenAI Logo"
className="w-72 h-72 mx-auto grow"
/>
</div>
);
case 3:
return (
<div className="flex flex-col items-center justify-center h-full">
<h2 className="text-3xl font-bold mb-2 text-center">
Thank you for signing in!
</h2>
<p className="text-white/70 mb-6 text-center px-24">
You're all set to start creating amazing content. Join our Discord
community to connect with other creators, share your work, and get
the latest updates.
</p>
<Button
variant="secondary"
onClick={() =>
window.open("https://discord.gg/75svZP2Vje", "_blank")
}
icon={faDiscord}
className="text-md bg-[#5865F2] hover:bg-[#6a76ff] rounded-xl"
>
Join our Discord
</Button>
</div>
);
default:
return null;
}
};
return createPortal(
<Transition appear show={isOpen}>
<div className="fixed inset-0 z-[100]">
<TransitionChild
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 cursor-pointer bg-black/80" />
</TransitionChild>
<div className="fixed inset-0 flex items-center justify-center p-4">
<TransitionChild
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<div
className="relative h-[660px] max-w-4xl w-full rounded-xl bg-[#2C2C2C] text-white shadow-lg border border-white/5"
onClick={(e) => e.stopPropagation()}
>
<div className="flex flex-col gap-4 p-8 h-full">
<span className="text-sm text-center opacity-60 font-medium">
Step {step} of {totalSteps}
</span>
{renderProgress()}
{renderStepContent()}
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
{step > 1 && step !== 3 ? (
<Button
variant="secondary"
onClick={handleBack}
disabled={isLoading}
>
Back
</Button>
) : null}
{step < totalSteps ? (
<Button
icon={step === 2 ? faRightToBracket : faArrowRight}
iconFlip={step !== 2}
onClick={handleNext}
loading={isLoading}
disabled={isLoading}
>
{step === 2 ? "Login with OpenAI" : "Continue"}
</Button>
) : (
<Button
icon={faArrowRight}
iconFlip={true}
onClick={handleClose}
>
Start Creating Now
</Button>
)}
</div>
</div>
</div>
</TransitionChild>
</div>
</div>
</Transition>,
document.body
);
}
export default LoginModal;
@@ -0,0 +1,13 @@
{
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
],
"extends": "../../../tsconfig.base.json"
}
@@ -0,0 +1,45 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"types": [
"node",
"@nx/react/typings/cssmodule.d.ts",
"@nx/react/typings/image.d.ts",
"vite/client"
],
"rootDir": "src",
"jsx": "react-jsx",
"tsBuildInfoFile": "dist/tsconfig.lib.tsbuildinfo"
},
"exclude": [
"out-tsc",
"dist",
"**/*.spec.ts",
"**/*.test.ts",
"**/*.spec.tsx",
"**/*.test.tsx",
"**/*.spec.js",
"**/*.test.js",
"**/*.spec.jsx",
"**/*.test.jsx",
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx"
],
"include": [
"src/**/*.js",
"src/**/*.jsx",
"src/**/*.ts",
"src/**/*.tsx"
]
}
@@ -0,0 +1,34 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./out-tsc/vitest",
"types": [
"vitest/globals",
"vitest/importMeta",
"vite/client",
"node",
"vitest"
],
"jsx": "react-jsx"
},
"include": [
"vite.config.ts",
"vite.config.mts",
"vitest.config.ts",
"vitest.config.mts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.test.tsx",
"src/**/*.spec.tsx",
"src/**/*.test.js",
"src/**/*.spec.js",
"src/**/*.test.jsx",
"src/**/*.spec.jsx",
"src/**/*.d.ts"
],
"references": [
{
"path": "./tsconfig.lib.json"
}
]
}
@@ -0,0 +1,49 @@
/// <reference types='vitest' />
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import * as path from 'path';
export default defineConfig(() => ({
root: __dirname,
cacheDir: '../../../node_modules/.vite/libs/components/login-modal',
plugins: [react(), dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json') })],
// Uncomment this if you are using workers.
// worker: {
// plugins: [ nxViteTsPaths() ],
// },
// Configuration for building your library.
// See: https://vitejs.dev/guide/build.html#library-mode
build: {
outDir: './dist',
emptyOutDir: true,
reportCompressedSize: true,
commonjsOptions: {
transformMixedEsModules: true,
},
lib: {
// Could also be a dictionary or array of multiple entry points.
entry: 'src/index.ts',
name: '@storyteller/ui-login-modal',
fileName: 'index',
// Change this to the formats you want to support.
// Don't forget to update your package.json as well.
formats: ['es' as const]
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['react','react-dom','react/jsx-runtime']
},
},
test: {
watch: false,
globals: true,
environment: 'jsdom',
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
reporters: ['default'],
coverage: {
reportsDirectory: './test-output/vitest/coverage',
provider: 'v8' as const,
}
},
}));
@@ -2,27 +2,21 @@
"files": [],
"include": [],
"references": [
{
"path": "../modal"
},
{
"path": "../select"
},
{
"path": "../../tauri-api"
},
{
"path": "../input"
},
{
"path": "../button"
},
{
"path": "../modal"
},
{
"path": "../../api"
},
{
"path": "../../tauri-api"
},
{
"path": "../../tauri-utils"
},
{
"path": "../../soundboard"
},
@@ -43,29 +43,23 @@
"src/**/*.tsx"
],
"references": [
{
"path": "../modal/tsconfig.lib.json"
},
{
"path": "../select/tsconfig.lib.json"
},
{
"path": "../../tauri-api/tsconfig.lib.json"
},
{
"path": "../input/tsconfig.lib.json"
},
{
"path": "../button/tsconfig.lib.json"
},
{
"path": "../modal/tsconfig.lib.json"
},
{
"path": "../../api/tsconfig.lib.json"
},
{
"path": "../../tauri-api/tsconfig.lib.json"
},
{
"path": "../../tauri-utils/tsconfig.lib.json"
},
{
"path": "../../soundboard/tsconfig.lib.json"
},
}
]
}
@@ -82,6 +82,7 @@ export const TabSelector: React.FC<TabSelectorProps> = ({
className={({ selected }) =>
twMerge(
"relative z-20 mx-0.5 min-w-max rounded-md border-2 border-transparent px-4 py-0.5 text-center text-sm font-semibold transition-all duration-200 ease-in-out",
"focus-visible:outline-none focus-visible:ring-0",
selected ? "text-white" : "text-gray-300 hover:text-white",
disabled ? "cursor-not-allowed opacity-60" : ""
)
+1 -1
View File
@@ -15,6 +15,6 @@
}
},
"dependencies": {
"@tauri-apps/api": "^2.5.0"
"@tauri-apps/api": "^2.4.0"
}
}
+12 -4
View File
@@ -2215,6 +2215,10 @@
"name": "@storyteller/ui-loading-spinner",
"version": "0.0.1"
},
"libs/components/login-modal": {
"name": "@storyteller/ui-login-modal",
"version": "0.0.1"
},
"libs/components/modal": {
"name": "@storyteller/ui-modal",
"version": "0.0.1"
@@ -2260,7 +2264,7 @@
"name": "@storyteller/tauri-api",
"version": "0.0.1",
"dependencies": {
"@tauri-apps/api": "^2.5.0"
"@tauri-apps/api": "^2.4.0"
}
},
"libs/tauri-utils": {
@@ -10320,6 +10324,10 @@
"resolved": "libs/components/loading-spinner",
"link": true
},
"node_modules/@storyteller/ui-login-modal": {
"resolved": "libs/components/login-modal",
"link": true
},
"node_modules/@storyteller/ui-modal": {
"resolved": "libs/components/modal",
"link": true
@@ -10941,9 +10949,9 @@
}
},
"node_modules/@tauri-apps/api": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.5.0.tgz",
"integrity": "sha512-Ldux4ip+HGAcPUmuLT8EIkk6yafl5vK0P0c0byzAKzxJh7vxelVtdPONjfgTm96PbN24yjZNESY8CKo8qniluA==",
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@tauri-apps/api/-/api-2.4.1.tgz",
"integrity": "sha512-5sYwZCSJb6PBGbBL4kt7CnE5HHbBqwH+ovmOW6ZVju3nX4E3JX6tt2kRklFEH7xMOIwR0btRkZktuLhKvyEQYg==",
"license": "Apache-2.0 OR MIT",
"funding": {
"type": "opencollective",
+3
View File
@@ -65,6 +65,9 @@
},
{
"path": "./libs/tauri-api"
},
{
"path": "./libs/components/login-modal"
}
]
}