From 0c2dd821610c867f6df93d530271ec91c2db3c1a Mon Sep 17 00:00:00 2001 From: BFlat Date: Tue, 12 May 2026 22:57:10 +0700 Subject: [PATCH] unprotect webapp pages and signup cta on generate (#1504) - public viewable pages - signup cta on generate if not logged in --- frontend/apps/artcraft-webapp/src/app/app.tsx | 26 ++-- .../CreateMediaPageShell.tsx | 59 +-------- .../generation-gallery/useGalleryData.ts | 11 +- .../src/components/signup-cta-modal.tsx | 118 ++++++++++++++++++ .../src/pages/create-image/create-image.tsx | 13 +- .../src/pages/create-vfx/create-vfx.tsx | 52 ++------ .../src/pages/create-video/create-video.tsx | 13 +- .../artcraft-webapp/src/pages/home/home.tsx | 16 ++- 8 files changed, 183 insertions(+), 125 deletions(-) create mode 100644 frontend/apps/artcraft-webapp/src/components/signup-cta-modal.tsx 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() {