mirror of
https://github.com/storytold/artcraft.git
synced 2026-10-21 01:12:40 -04:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95a6e9f19d | ||
|
|
7433096a53 | ||
|
|
e76aea2c12 | ||
|
|
0e111c4233 | ||
|
|
1b71199bf2 | ||
|
|
bc2e44efdf |
@@ -1,8 +1,6 @@
|
||||
import { useSignals } from "@preact/signals-react/runtime";
|
||||
|
||||
import { AUTH_STATUS } from "~/enums";
|
||||
import { authentication } from "~/signals";
|
||||
|
||||
import { ButtonLink } from "~/components";
|
||||
import ProfileDropdown from "./ProfileDropdown";
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ import { OnboardingHelper } from "./comps/OnboardingHelper";
|
||||
import { FocalLengthDisplay } from "./comps/FocalLengthDisplay/FocalLengthDisplay";
|
||||
import { appTabId, setAppTabId } from "~/signals/appTab";
|
||||
import { LoginModal } from "@storyteller/ui-login-modal";
|
||||
import {
|
||||
disableHotkeyInput,
|
||||
enableHotkeyInput,
|
||||
DomLevels,
|
||||
} from "./signals/hotkeys";
|
||||
import { AUTH_STATUS } from "../../enums/authentication";
|
||||
import { authentication } from "../../signals/authentication/authentication";
|
||||
|
||||
export const PageEditor = () => {
|
||||
useSignals();
|
||||
@@ -156,26 +163,22 @@ export const PageEditor = () => {
|
||||
<Timeline />
|
||||
</div> */}
|
||||
|
||||
{/* <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!"
|
||||
videoSrc="/resources/videos/artcraft-3d-demo.mp4"
|
||||
buttonText="Sign in to OpenAI to get started"
|
||||
buttonOnClick={async () => {
|
||||
if (IsDesktopApp()) {
|
||||
await invoke("open_sora_login_command");
|
||||
} else {
|
||||
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"
|
||||
onOpenChange={(isOpen: boolean) => {
|
||||
if (isOpen) {
|
||||
disableHotkeyInput(DomLevels.DIALOGUE);
|
||||
} else {
|
||||
enableHotkeyInput(DomLevels.DIALOGUE);
|
||||
}
|
||||
}}
|
||||
onArtCraftAuthSuccess={(userInfo: any) => {
|
||||
authentication.status.value = AUTH_STATUS.LOGGED_IN;
|
||||
authentication.userInfo.value = userInfo;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -116,8 +116,10 @@ export const GalleryModal = React.memo(
|
||||
setUsername(session.data.user.username);
|
||||
}
|
||||
};
|
||||
getUsername();
|
||||
}, [usersApi]);
|
||||
if (isOpen) {
|
||||
getUsername();
|
||||
}
|
||||
}, [usersApi, isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadItems = async () => {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { Input } from "@storyteller/ui-input";
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { faUser, faKey, faEnvelope } from "@fortawesome/pro-solid-svg-icons";
|
||||
|
||||
interface ArtCraftSignUpProps {
|
||||
onSubmit: (
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
passwordConfirmation: string
|
||||
) => void;
|
||||
isSignUp: boolean;
|
||||
onToggleMode: () => void;
|
||||
formRef?: React.RefObject<HTMLFormElement | null>;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
export const ArtCraftSignUp = ({
|
||||
onSubmit,
|
||||
isSignUp,
|
||||
onToggleMode,
|
||||
formRef,
|
||||
errorMessage,
|
||||
}: ArtCraftSignUpProps) => {
|
||||
const [localError, setLocalError] = useState<string | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalError(errorMessage);
|
||||
}, [errorMessage]);
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
const form = e.currentTarget;
|
||||
if (isSignUp) {
|
||||
const username = (form.elements.namedItem("username") as HTMLInputElement)
|
||||
.value;
|
||||
const email = (form.elements.namedItem("email") as HTMLInputElement)
|
||||
.value;
|
||||
const password = (form.elements.namedItem("password") as HTMLInputElement)
|
||||
.value;
|
||||
const confirmPassword = (
|
||||
form.elements.namedItem("confirmPassword") as HTMLInputElement
|
||||
).value;
|
||||
if (password !== confirmPassword) {
|
||||
setLocalError("Passwords do not match.");
|
||||
return;
|
||||
}
|
||||
onSubmit(username, email, password, confirmPassword);
|
||||
} else {
|
||||
const usernameOrEmail = (
|
||||
form.elements.namedItem("usernameOrEmail") as HTMLInputElement
|
||||
).value;
|
||||
const password = (form.elements.namedItem("password") as HTMLInputElement)
|
||||
.value;
|
||||
onSubmit(usernameOrEmail, "", password, "");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h2 className="text-3xl font-bold mb-3 text-center">
|
||||
{isSignUp ? "Sign up for ArtCraft" : "Log in to ArtCraft"}
|
||||
</h2>
|
||||
<form
|
||||
className="flex flex-col gap-3 w-full max-w-md"
|
||||
onSubmit={handleSubmit}
|
||||
ref={formRef}
|
||||
>
|
||||
{isSignUp ? (
|
||||
<>
|
||||
<Input
|
||||
label="Username"
|
||||
icon={faUser}
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Input
|
||||
label="Email"
|
||||
icon={faEnvelope}
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
required={isSignUp}
|
||||
style={isSignUp ? {} : { display: "none" }}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Input
|
||||
label="Email or Username"
|
||||
icon={faUser}
|
||||
name="usernameOrEmail"
|
||||
placeholder="Email or Username"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Input
|
||||
label="Password"
|
||||
icon={faKey}
|
||||
type="password"
|
||||
name="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
{isSignUp && (
|
||||
<Input
|
||||
label="Confirm Password"
|
||||
icon={faKey}
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
placeholder="Confirm Password"
|
||||
required
|
||||
autoComplete="off"
|
||||
/>
|
||||
)}
|
||||
<button type="submit" className="hidden" />
|
||||
<div className="relative mt-3">
|
||||
<div className="flex gap-2 items-center justify-center p-2 bg-white/5 rounded-lg">
|
||||
<p className="text-sm opacity-80">
|
||||
{isSignUp ? "Already have an account?" : "Don't have an account?"}{" "}
|
||||
</p>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="p-0 bg-transparent hover:bg-transparent"
|
||||
type="button"
|
||||
onClick={onToggleMode}
|
||||
>
|
||||
<span className="text-sm text-primary-400 underline">
|
||||
{isSignUp ? "Log in" : "Sign up"}
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
{localError && (
|
||||
<div className="absolute -bottom-10 left-0 right-0 text-red text-sm text-center mb-2 font-semibold">
|
||||
{localError}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Button } from "@storyteller/ui-button";
|
||||
import { Transition, TransitionChild } from "@headlessui/react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import {
|
||||
faArrowRight,
|
||||
faRightToBracket,
|
||||
@@ -14,12 +14,15 @@ import {
|
||||
useSoraLoginListener,
|
||||
} from "@storyteller/tauri-api";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
|
||||
import { ArtCraftSignUp } from "./ArtCraftSignUp";
|
||||
import { UsersApi } from "@storyteller/api";
|
||||
interface LoginModalProps {
|
||||
onClose: () => void;
|
||||
videoSrc2D?: string;
|
||||
videoSrc3D?: string;
|
||||
openAiLogo?: string;
|
||||
onOpenChange?: (isOpen: boolean) => void;
|
||||
onArtCraftAuthSuccess?: (userInfo: any) => void;
|
||||
}
|
||||
|
||||
export function LoginModal({
|
||||
@@ -27,31 +30,140 @@ export function LoginModal({
|
||||
videoSrc2D,
|
||||
videoSrc3D,
|
||||
openAiLogo,
|
||||
onOpenChange,
|
||||
onArtCraftAuthSuccess,
|
||||
}: LoginModalProps) {
|
||||
const [step, setStep] = useState(1);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isLoggedInArtCraft, setIsLoggedInArtCraft] = useState(false);
|
||||
const [isSignUp, setIsSignUp] = useState(true);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
const totalSteps = isLoggedInArtCraft ? 3 : 4;
|
||||
const artCraftFormRef = useRef<HTMLFormElement>(null);
|
||||
|
||||
const totalSteps = 3;
|
||||
const initSession = async () => {
|
||||
const result = await CheckSoraSession();
|
||||
const sessionExists = result.state === SoraSessionState.Valid;
|
||||
return sessionExists;
|
||||
};
|
||||
|
||||
const checkArtCraftLogin = async () => {
|
||||
const usersApi = new UsersApi();
|
||||
const session = await usersApi.GetSession();
|
||||
const loggedIn = session.data?.loggedIn;
|
||||
return loggedIn;
|
||||
};
|
||||
|
||||
useSoraLoginListener((payload) => {
|
||||
console.log("Login success!", payload);
|
||||
setStep(3);
|
||||
if (isLoggedInArtCraft) {
|
||||
setStep(4);
|
||||
} else {
|
||||
setStep(3);
|
||||
}
|
||||
});
|
||||
|
||||
// Check session on component mount
|
||||
useEffect(() => {
|
||||
const initSession = async () => {
|
||||
const result = await CheckSoraSession();
|
||||
const sessionExists = result.state === SoraSessionState.Valid;
|
||||
if (!sessionExists) {
|
||||
setIsOpen(true);
|
||||
setStep(1);
|
||||
// Run both checks in parallel
|
||||
Promise.all([initSession(), checkArtCraftLogin()]).then(
|
||||
([soraSessionExists, loggedIn]) => {
|
||||
if (soraSessionExists && !loggedIn) {
|
||||
// Sora session exists, but not logged in to ArtCraft
|
||||
setIsOpen(true);
|
||||
setStep(3);
|
||||
} else if (!soraSessionExists) {
|
||||
// No Sora session, start at step 1
|
||||
setIsOpen(true);
|
||||
setStep(1);
|
||||
} else if (loggedIn) {
|
||||
// Already logged in, no need to show modal
|
||||
setIsLoggedInArtCraft(true);
|
||||
// setIsOpen(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
initSession();
|
||||
);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (onOpenChange) onOpenChange(isOpen);
|
||||
}, [isOpen, onOpenChange]);
|
||||
|
||||
const handleArtCraftAuth = async (
|
||||
username: string,
|
||||
email: string,
|
||||
password: string,
|
||||
passwordConfirmation: string
|
||||
) => {
|
||||
setIsLoading(true);
|
||||
const usersApi = new UsersApi();
|
||||
try {
|
||||
let signupResponse, loginResponse;
|
||||
if (isSignUp) {
|
||||
// Sign up
|
||||
signupResponse = await usersApi.Signup({
|
||||
username,
|
||||
email,
|
||||
password,
|
||||
passwordConfirmation,
|
||||
});
|
||||
if (!signupResponse.success) {
|
||||
// TODO: Add error handling UI
|
||||
console.error(signupResponse.errorMessage || "Signup failed");
|
||||
setErrorMessage(
|
||||
signupResponse.errorMessage || "Signup failed, please try again."
|
||||
);
|
||||
return;
|
||||
}
|
||||
// Automatically login after successful signup
|
||||
loginResponse = await usersApi.Login({
|
||||
usernameOrEmail: username || email,
|
||||
password,
|
||||
});
|
||||
if (!loginResponse.success) {
|
||||
// TODO: Add error handling UI
|
||||
console.error(
|
||||
loginResponse.errorMessage || "Login after signup failed"
|
||||
);
|
||||
setErrorMessage(
|
||||
loginResponse.errorMessage ||
|
||||
"Login after signup failed, please try again. "
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Login
|
||||
loginResponse = await usersApi.Login({
|
||||
usernameOrEmail: username || email,
|
||||
password,
|
||||
});
|
||||
if (!loginResponse.success) {
|
||||
// TODO: Add error handling UI
|
||||
console.error(loginResponse.errorMessage || "Login failed");
|
||||
setErrorMessage(
|
||||
loginResponse.errorMessage || "Login failed, please try again."
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
setStep(4);
|
||||
|
||||
const session = await usersApi.GetSession();
|
||||
const userInfo = session.data?.user;
|
||||
if (onArtCraftAuthSuccess && userInfo) {
|
||||
onArtCraftAuthSuccess(userInfo);
|
||||
}
|
||||
} catch (e) {
|
||||
// handle error (show error message, etc.)
|
||||
// TODO: Add error handling UI
|
||||
console.error(e);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setIsOpen(false);
|
||||
onClose();
|
||||
@@ -63,8 +175,6 @@ export function LoginModal({
|
||||
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));
|
||||
const result = await CheckSoraSession();
|
||||
const sessionExists = result.state === SoraSessionState.Valid;
|
||||
@@ -73,10 +183,8 @@ export function LoginModal({
|
||||
}
|
||||
} 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));
|
||||
const sessionExists = true;
|
||||
|
||||
if (sessionExists) {
|
||||
setStep(step + 1);
|
||||
}
|
||||
@@ -86,7 +194,12 @@ export function LoginModal({
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
} else if (step < totalSteps) {
|
||||
} else if (step === 3) {
|
||||
// Trigger the form submit in ArtCraftSignUp
|
||||
if (artCraftFormRef.current) {
|
||||
artCraftFormRef.current.requestSubmit();
|
||||
}
|
||||
} else {
|
||||
setStep(step + 1);
|
||||
}
|
||||
};
|
||||
@@ -111,6 +224,10 @@ export function LoginModal({
|
||||
|
||||
// Step content rendering
|
||||
const renderStepContent = () => {
|
||||
if (step === 3 && isLoggedInArtCraft) {
|
||||
setStep(4);
|
||||
return null;
|
||||
}
|
||||
switch (step) {
|
||||
case 1:
|
||||
return (
|
||||
@@ -177,6 +294,16 @@ export function LoginModal({
|
||||
</div>
|
||||
);
|
||||
case 3:
|
||||
return (
|
||||
<ArtCraftSignUp
|
||||
onSubmit={handleArtCraftAuth}
|
||||
isSignUp={isSignUp}
|
||||
onToggleMode={() => setIsSignUp((prev) => !prev)}
|
||||
formRef={artCraftFormRef}
|
||||
errorMessage={errorMessage}
|
||||
/>
|
||||
);
|
||||
case 4:
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<h2 className="text-3xl font-bold mb-2 text-center">
|
||||
@@ -237,7 +364,7 @@ export function LoginModal({
|
||||
{renderProgress()}
|
||||
{renderStepContent()}
|
||||
<div className="flex items-end justify-center gap-2.5 mt-10 grow">
|
||||
{step > 1 && step !== 3 ? (
|
||||
{step === 2 ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={handleBack}
|
||||
@@ -255,7 +382,13 @@ export function LoginModal({
|
||||
loading={isLoading}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{step === 2 ? "Login with OpenAI" : "Continue"}
|
||||
{step === 2
|
||||
? "Login with OpenAI"
|
||||
: step === 3
|
||||
? isSignUp
|
||||
? "Sign up"
|
||||
: "Login"
|
||||
: "Continue"}
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
@@ -278,4 +411,4 @@ export function LoginModal({
|
||||
);
|
||||
}
|
||||
|
||||
export default LoginModal;
|
||||
export default LoginModal;
|
||||
Reference in New Issue
Block a user