diff --git a/frontend/apps/artcraft-webapp/src/app/app.tsx b/frontend/apps/artcraft-webapp/src/app/app.tsx
index 4a28811aed..e9deea17d5 100644
--- a/frontend/apps/artcraft-webapp/src/app/app.tsx
+++ b/frontend/apps/artcraft-webapp/src/app/app.tsx
@@ -31,6 +31,7 @@ import {
} from "../components/ui/sidebar";
import { AppSidebar } from "../components/sidebar/app-sidebar";
import { TopBar } from "../components/topbar/topbar";
+import { SignupCtaModal } from "../components/signup-cta-modal";
import { useSession } from "../lib/session";
function ScrollToTop() {
@@ -112,19 +113,22 @@ export function App() {
} />
} />
- {/* Protected — auth gate + sidebar/topbar chrome */}
- }>
- }>
- } />
- } />
- } />
- } />
+ {/* Public — sidebar/topbar chrome, but no auth gate. Generate actions
+ inside the create pages pop a signup CTA modal for logged-out users. */}
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+ {/* Protected — sign-in required (user-owned content / billing flows) */}
+ }>
} />
} />
- } />
} />
} />
- } />
} />
} />
} />
@@ -141,11 +145,13 @@ export function App() {
path="/portal_closed"
element={}
/>
- } />
+
+ } />
+
>
);
}
diff --git a/frontend/apps/artcraft-webapp/src/components/generation-gallery/CreateMediaPageShell.tsx b/frontend/apps/artcraft-webapp/src/components/generation-gallery/CreateMediaPageShell.tsx
index 6af084706b..4c13d1f581 100644
--- a/frontend/apps/artcraft-webapp/src/components/generation-gallery/CreateMediaPageShell.tsx
+++ b/frontend/apps/artcraft-webapp/src/components/generation-gallery/CreateMediaPageShell.tsx
@@ -1,11 +1,6 @@
import { type ReactNode } from "react";
-import { Link } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import {
- faSpinnerThird,
- type IconDefinition,
-} from "@fortawesome/pro-solid-svg-icons";
-import { Button } from "@storyteller/ui-button";
+import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
import { type PopoverItem } from "@storyteller/ui-popover";
import Seo from "../../components/seo";
@@ -13,13 +8,10 @@ interface CreateMediaPageShellProps {
// SEO
title: string;
description: string;
- // Auth state
+ // Auth state — `authChecked` gates the initial spinner so we don't flash
+ // logged-out chrome while the session resolves. Pages stay viewable for
+ // logged-out users; the signup CTA modal is triggered at generate time.
authChecked: boolean;
- isLoggedIn: boolean;
- // Unauthenticated state
- heroIcon: IconDefinition;
- heroTitle: string;
- heroSubtitle: string;
// Content
hasContent: boolean;
emptyStateTitle: string;
@@ -40,10 +32,6 @@ export function CreateMediaPageShell({
title,
description,
authChecked,
- isLoggedIn,
- heroIcon,
- heroTitle,
- heroSubtitle,
hasContent,
emptyStateTitle,
emptyStateSubtitle,
@@ -64,45 +52,6 @@ export function CreateMediaPageShell({
);
}
- if (!isLoggedIn) {
- return (
-
-
-
-
-
-
{heroTitle}
-
- {heroSubtitle}
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
return (
diff --git a/frontend/apps/artcraft-webapp/src/components/generation-gallery/useGalleryData.ts b/frontend/apps/artcraft-webapp/src/components/generation-gallery/useGalleryData.ts
index 87d0edbc47..0f48818e7d 100644
--- a/frontend/apps/artcraft-webapp/src/components/generation-gallery/useGalleryData.ts
+++ b/frontend/apps/artcraft-webapp/src/components/generation-gallery/useGalleryData.ts
@@ -120,14 +120,19 @@ export function useGalleryData(options: {
[username, filterMediaClasses, pageIndex, api, mapApiItem, excludeUploads],
);
- // Initial load + filter change
+ // Initial load + filter change. When logged out (no username), clear the
+ // loading flag so the shell renders the empty state instead of a spinner.
useEffect(() => {
- if (!username) return;
setItems([]);
setPageIndex(0);
+ isLoadingRef.current = false;
+ if (!username) {
+ setHasMore(false);
+ setIsInitialLoading(false);
+ return;
+ }
setHasMore(true);
setIsInitialLoading(true);
- isLoadingRef.current = false;
loadItems(true);
}, [username, JSON.stringify(filterMediaClasses)]); // eslint-disable-line react-hooks/exhaustive-deps
diff --git a/frontend/apps/artcraft-webapp/src/components/signup-cta-modal.tsx b/frontend/apps/artcraft-webapp/src/components/signup-cta-modal.tsx
new file mode 100644
index 0000000000..eb111ac305
--- /dev/null
+++ b/frontend/apps/artcraft-webapp/src/components/signup-cta-modal.tsx
@@ -0,0 +1,118 @@
+import { Link, useLocation } from "react-router-dom";
+import { create } from "zustand";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faCheck } from "@fortawesome/pro-solid-svg-icons";
+import { Modal } from "@storyteller/ui-modal";
+import { Button } from "@storyteller/ui-button";
+import { useSession } from "../lib/session";
+
+interface SignupCtaState {
+ isOpen: boolean;
+ open: () => void;
+ close: () => void;
+}
+
+const useSignupCtaStore = create
((set) => ({
+ isOpen: false,
+ open: () => set({ isOpen: true }),
+ close: () => set({ isOpen: false }),
+}));
+
+/**
+ * Hook that exposes the logged-in flag plus an imperative `openSignupCta()`
+ * trigger. Use it at the top of a generate handler:
+ *
+ * const { loggedIn, openSignupCta } = useSignupCta();
+ * if (!loggedIn) { openSignupCta(); return; }
+ */
+export function useSignupCta(): {
+ loggedIn: boolean;
+ openSignupCta: () => void;
+} {
+ const { loggedIn } = useSession();
+ const openSignupCta = useSignupCtaStore((s) => s.open);
+ return { loggedIn, openSignupCta };
+}
+
+const PERKS: string[] = [
+ "Generate images and videos with top AI models",
+ "Save your work and access it from any device",
+];
+
+export function SignupCtaModal() {
+ const isOpen = useSignupCtaStore((s) => s.isOpen);
+ const close = useSignupCtaStore((s) => s.close);
+ const location = useLocation();
+ const from = encodeURIComponent(location.pathname + location.search);
+
+ return (
+
+
+ {/* Off-center ambient glow — feels designed, not generic */}
+
+
+
+
+
+ Start crafting in seconds.
+
+
+ Free to try. Sign up and pick up where you left off.
+
+
+
+ {PERKS.map((perk) => (
+ -
+
+
+
+ {perk}
+
+ ))}
+
+
+
+
+
+
+
+ Already have an account?{" "}
+ Log in
+
+
+
+
+
+ );
+}
diff --git a/frontend/apps/artcraft-webapp/src/pages/create-image/create-image.tsx b/frontend/apps/artcraft-webapp/src/pages/create-image/create-image.tsx
index b4a8ad4f5a..b7fb66a510 100644
--- a/frontend/apps/artcraft-webapp/src/pages/create-image/create-image.tsx
+++ b/frontend/apps/artcraft-webapp/src/pages/create-image/create-image.tsx
@@ -1,5 +1,4 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
-import { faImage } from "@fortawesome/pro-solid-svg-icons";
import { FilterMediaClasses } from "@storyteller/api";
import type { OmniGenImageModelInfo } from "@storyteller/api";
import { PopoverMenu, type PopoverItem } from "@storyteller/ui-popover";
@@ -30,6 +29,7 @@ import {
useOmniGenImageModels,
getModelCreatorIconPath,
} from "../../lib/omni-gen-hooks";
+import { useSignupCta } from "../../components/signup-cta-modal";
// ── Constants ────────────────────────────────────────────────────────────
@@ -63,6 +63,7 @@ function buildModelPopoverItems(
export default function CreateImage() {
const { user, authChecked } = useAuthCheck();
+ const { loggedIn, openSignupCta } = useSignupCta();
const { promptBoxRef, promptHeight } = usePromptHeight();
// Fetch models from API
@@ -277,6 +278,10 @@ export default function CreateImage() {
);
const handleGenerate = useCallback(async () => {
+ if (!loggedIn) {
+ openSignupCta();
+ return;
+ }
if (!prompt.trim() || isGenerating || !selectedModel) return;
setIsGenerating(true);
@@ -335,6 +340,8 @@ export default function CreateImage() {
setIsGenerating(false);
}
}, [
+ loggedIn,
+ openSignupCta,
prompt,
isGenerating,
selectedModel,
@@ -358,10 +365,6 @@ export default function CreateImage() {
title="Create Image - ArtCraft"
description="Generate stunning AI images with ArtCraft"
authChecked={authChecked}
- isLoggedIn={!!user}
- heroIcon={faImage}
- heroTitle="Create Image"
- heroSubtitle="Sign in to generate stunning AI images with multiple models"
hasContent={hasContent}
emptyStateTitle="Generate Image"
emptyStateSubtitle="Add a prompt, then generate"
diff --git a/frontend/apps/artcraft-webapp/src/pages/create-vfx/create-vfx.tsx b/frontend/apps/artcraft-webapp/src/pages/create-vfx/create-vfx.tsx
index d28933a0e6..26a9229d8f 100644
--- a/frontend/apps/artcraft-webapp/src/pages/create-vfx/create-vfx.tsx
+++ b/frontend/apps/artcraft-webapp/src/pages/create-vfx/create-vfx.tsx
@@ -1,8 +1,6 @@
import { useCallback, useEffect, useRef, useState } from "react";
-import { Link } from "react-router-dom";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faSpinnerThird, faSparkles } from "@fortawesome/pro-solid-svg-icons";
-import { Button } from "@storyteller/ui-button";
import {
PromptBoxVFX,
VFXResultCard,
@@ -10,6 +8,7 @@ import {
} from "@storyteller/ui-vfx";
import Seo from "../../components/seo";
import { useAuthCheck } from "../../components/generation-gallery";
+import { useSignupCta } from "../../components/signup-cta-modal";
import { toast } from "../../components/toast/toast";
import { uploadImage } from "../../components/prompt-box/upload-image";
import { uploadVideo } from "../../components/prompt-box/upload-media";
@@ -21,6 +20,7 @@ import {
export default function CreateVFX() {
const { user, authChecked } = useAuthCheck();
+ const { loggedIn, openSignupCta } = useSignupCta();
const history = useVFXStore((s) => s.history);
const startResult = useVFXStore((s) => s.startResult);
@@ -190,6 +190,10 @@ export default function CreateVFX() {
}, [user, completeResult, failResult, seedFromSession, updateMediaForResult]);
const handleSubmit = useCallback(async () => {
+ if (!loggedIn) {
+ openSignupCta();
+ return;
+ }
if (!source || !reference || isSubmitting) return;
setIsSubmitting(true);
const id = startResult();
@@ -230,6 +234,8 @@ export default function CreateVFX() {
);
pollersRef.current.set(id, cancel);
}, [
+ loggedIn,
+ openSignupCta,
source,
reference,
prompt,
@@ -251,48 +257,6 @@ export default function CreateVFX() {
);
}
- if (!user) {
- return (
-
-
-
-
-
-
Background Change
-
- Sign in to swap the backdrop of a video using a reference image.
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-
return (
{
+ if (!loggedIn) {
+ openSignupCta();
+ return;
+ }
if (
!prompt.trim() ||
isGeneratingRef.current ||
@@ -849,6 +854,8 @@ export default function CreateVideo() {
setIsGenerating(false);
isGeneratingRef.current = false;
}, [
+ loggedIn,
+ openSignupCta,
prompt,
needsImage,
isReferenceMode,
@@ -888,10 +895,6 @@ export default function CreateVideo() {
title="Create Video - ArtCraft"
description="Generate stunning AI videos with ArtCraft"
authChecked={authChecked}
- isLoggedIn={!!user}
- heroIcon={faFilm}
- heroTitle="Create Video"
- heroSubtitle="Sign in to generate stunning AI videos with multiple models"
hasContent={hasContent}
emptyStateTitle="Generate Video"
emptyStateSubtitle="Add a prompt, then generate"
diff --git a/frontend/apps/artcraft-webapp/src/pages/home/home.tsx b/frontend/apps/artcraft-webapp/src/pages/home/home.tsx
index eb349367be..99c32be680 100644
--- a/frontend/apps/artcraft-webapp/src/pages/home/home.tsx
+++ b/frontend/apps/artcraft-webapp/src/pages/home/home.tsx
@@ -14,6 +14,8 @@ type AppCard = {
href: string;
icon: IconDefinition;
accent: string;
+ iconBg: string;
+ iconColor: string;
};
const APPS: AppCard[] = [
@@ -23,6 +25,8 @@ const APPS: AppCard[] = [
href: "/create-image",
icon: faImage,
accent: "from-blue-500/20 to-blue-500/0",
+ iconBg: "bg-blue-500/25 border-blue-400/30",
+ iconColor: "text-blue-300",
},
{
label: "Video",
@@ -30,6 +34,8 @@ const APPS: AppCard[] = [
href: "/create-video",
icon: faVideo,
accent: "from-purple-500/20 to-purple-500/0",
+ iconBg: "bg-purple-500/25 border-purple-400/30",
+ iconColor: "text-purple-300",
},
{
label: "Background Change",
@@ -37,6 +43,8 @@ const APPS: AppCard[] = [
href: "/background-change",
icon: faWandMagicSparkles,
accent: "from-emerald-500/20 to-emerald-500/0",
+ iconBg: "bg-emerald-500/25 border-emerald-400/30",
+ iconColor: "text-emerald-300",
},
];
@@ -54,14 +62,16 @@ export function Home() {
-
+
@@ -71,7 +81,7 @@ export function Home() {