diff --git a/frontend/apps/artcraft-webapp/public/images/artcraft-icon.png b/frontend/apps/artcraft-webapp/public/images/artcraft-icon.png
new file mode 100644
index 0000000000..ded60e7ef4
Binary files /dev/null and b/frontend/apps/artcraft-webapp/public/images/artcraft-icon.png differ
diff --git a/frontend/apps/artcraft-webapp/src/app/app.tsx b/frontend/apps/artcraft-webapp/src/app/app.tsx
index d2651c9390..78c016a82d 100644
--- a/frontend/apps/artcraft-webapp/src/app/app.tsx
+++ b/frontend/apps/artcraft-webapp/src/app/app.tsx
@@ -20,6 +20,8 @@ import Pricing from "../pages/pricing";
import Support from "../pages/support/support";
import Login from "../pages/login";
import Signup from "../pages/signup";
+import SetPassword from "../pages/set-password";
+import { AuthLayout } from "../components/auth";
import ForgotPassword, { VerifyReset } from "../pages/forgot-password";
import Welcome from "../pages/welcome";
import Onboarding from "../pages/onboarding";
@@ -118,9 +120,14 @@ export function App() {
<>
- {/* Public — no chrome, no auth gate */}
- } />
- } />
+ {/* Public — no chrome, no auth gate. login/signup share a layout route
+ so the showcase pane (and its video) stays mounted when toggling
+ between them. */}
+ }>
+ } />
+ } />
+
+ } />
} />
} />
diff --git a/frontend/apps/artcraft-webapp/src/components/auth/GoogleLoginButton.tsx b/frontend/apps/artcraft-webapp/src/components/auth/GoogleLoginButton.tsx
index ae0586573e..d54b1fbe25 100644
--- a/frontend/apps/artcraft-webapp/src/components/auth/GoogleLoginButton.tsx
+++ b/frontend/apps/artcraft-webapp/src/components/auth/GoogleLoginButton.tsx
@@ -57,9 +57,11 @@ export function GoogleLoginButton({
};
return (
-
+
{/* Custom visual button - shown behind */}
-
+
{isLoading ? (
- Continue with Google
+ Continue with Google
>
)}
diff --git a/frontend/apps/artcraft-webapp/src/components/auth/auth-layout.tsx b/frontend/apps/artcraft-webapp/src/components/auth/auth-layout.tsx
new file mode 100644
index 0000000000..65de77fda7
--- /dev/null
+++ b/frontend/apps/artcraft-webapp/src/components/auth/auth-layout.tsx
@@ -0,0 +1,109 @@
+import { ReactNode } from "react";
+import { Outlet, useNavigate } from "react-router-dom";
+import { faArrowLeft } from "@fortawesome/pro-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { TruchetPattern } from "@storyteller/ui-vfx";
+import { AuthShowcase } from "./auth-showcase";
+import { useMediaQuery } from "../ui/use-media-query";
+
+/**
+ * Persistent shell for the auth pages. Rendered as a layout route so the
+ * showcase pane (and its video) stays mounted while the form pane swaps
+ * between login and signup via — toggling between the two no longer
+ * reloads the showcase video from the start. Each page renders its own header
+ * (AuthHeader) and footer (AuthFooter) into the outlet.
+ */
+export const AuthLayout = () => {
+ // Only mount the showcase on wide screens (matches the `lg` breakpoint) so
+ // mobile never downloads the demo videos.
+ const showShowcase = useMediaQuery("(min-width: 1024px)");
+ const navigate = useNavigate();
+
+ const handleBack = () => {
+ navigate("/");
+ };
+
+ return (
+
+
+
+
+
+
+
+
+ {/* Form pane — only the inner content swaps per route (via Outlet), so
+ this pane and the showcase beside it stay mounted across the
+ login/signup toggle. */}
+
+ {/* Vimeo background embed (no controls) scaled to cover the pane. The
+ iframe is forced to 16:9 and sized to the larger cover dimension via
+ container-query units, then centered so the overflow crops evenly. */}
+
+
+ {/* Legibility gradient behind the caption */}
+
+
+ {/* Caption */}
+
+
+ One of the cheapest
+
+
+ Seedance 2.0 Video Generation
+
+
+ Generate jaw-dropping AI videos with Seedance 2.0.
+
+
+
+ );
+};
diff --git a/frontend/apps/artcraft-webapp/src/components/auth/index.ts b/frontend/apps/artcraft-webapp/src/components/auth/index.ts
index 1b970188c8..fbe2826eb6 100644
--- a/frontend/apps/artcraft-webapp/src/components/auth/index.ts
+++ b/frontend/apps/artcraft-webapp/src/components/auth/index.ts
@@ -1,2 +1,4 @@
export * from "./signup-form";
export * from "./GoogleLoginButton";
+export * from "./auth-layout";
+export * from "./auth-showcase";
diff --git a/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx b/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx
index f529ae7aed..830f844ea8 100644
--- a/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx
+++ b/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx
@@ -7,6 +7,7 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { Button } from "@storyteller/ui-button";
import { Input } from "@storyteller/ui-input";
import { useState } from "react";
+import { useNavigate } from "react-router-dom";
import { UsersApi } from "@storyteller/api";
import {
getLandingUrl,
@@ -14,6 +15,8 @@ import {
getReferralUsername,
getReferrer,
} from "@storyteller/common";
+import { refreshSession, useSessionStore } from "../../lib/session";
+import { GoogleLoginButton } from "./GoogleLoginButton";
interface SignupFormProps {
onSuccess: (isNewUser?: boolean) => void;
@@ -28,6 +31,7 @@ export const SignupForm = ({
className = "",
autoFocus = false,
}: SignupFormProps) => {
+ const navigate = useNavigate();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
@@ -81,36 +85,24 @@ export const SignupForm = ({
}
};
- //const handleGoogleSuccess = (isNewUser: boolean) => {
- // onSuccess(isNewUser);
- //};
+ const handleGoogleSuccess = async () => {
+ // Refresh so the session reflects the new cookie (and whether the account
+ // still needs a password), then route accordingly. New SSO users with no
+ // password go set one; everyone else goes home.
+ await refreshSession(true);
+ if (useSessionStore.getState().passwordNotSet) {
+ navigate("/set-password");
+ } else {
+ navigate("/");
+ }
+ };
- //const handleGoogleError = (errorMessage: string) => {
- // setError(errorMessage);
- //};
+ const handleGoogleError = (message: string) => {
+ setError(message);
+ };
return (