66 * editor, which is the one finishing surface the product has.
77 */
88
9- import { useCallback , useMemo , useState } from "react" ;
9+ import { useCallback , useMemo , useRef , useState } from "react" ;
1010import { useNavigate } from "react-router-dom" ;
1111import { useTheme } from "@mui/material/styles" ;
1212import TheatersRoundedIcon from "@mui/icons-material/TheatersRounded" ;
@@ -56,13 +56,15 @@ const PathCard = ({
5656 description,
5757 cta,
5858 busy,
59+ disabled,
5960 onStart
6061} : {
6162 icon : React . ReactNode ;
6263 title : string ;
6364 description : string ;
6465 cta : string ;
6566 busy : boolean ;
67+ disabled : boolean ;
6668 onStart : ( ) => void ;
6769} ) => (
6870 < Card
@@ -78,7 +80,7 @@ const PathCard = ({
7880 < Text size = "small" color = "secondary" >
7981 { description }
8082 </ Text >
81- < EditorButton variant = "contained" onClick = { onStart } disabled = { busy } >
83+ < EditorButton variant = "contained" onClick = { onStart } disabled = { disabled } >
8284 { busy ? "Creating…" : cta }
8385 </ EditorButton >
8486 </ FlexColumn >
@@ -94,7 +96,9 @@ const StudioHome = () => {
9496
9597 const storyboards = useStoryboards ( ) ;
9698 const scripts = useScripts ( ) ;
97- const timelines = useTimelines ( "default" ) ;
99+ // No project filter: storyboards and scripts list all the user's documents,
100+ // so the merged recent list scopes timelines the same way.
101+ const timelines = useTimelines ( ) ;
98102
99103 const recent = useMemo < RecentProject [ ] > ( ( ) => {
100104 const rows : RecentProject [ ] = [
@@ -122,20 +126,34 @@ const StudioHome = () => {
122126 . slice ( 0 , 12 ) ;
123127 } , [ storyboards . data , scripts . data , timelines . data ] ) ;
124128
129+ // One creation at a time: both cards disable while either runs, and the
130+ // handlers guard on a ref as well so a double-activation can't create two
131+ // projects or race the navigation.
132+ const creatingRef = useRef ( false ) ;
125133 const startStoryboard = useCallback ( ( ) => {
134+ if ( creatingRef . current ) return ;
135+ creatingRef . current = true ;
126136 setCreating ( "storyboard" ) ;
127137 createStoryboard
128138 . mutateAsync ( { name : "Untitled storyboard" , projectId : "default" } )
129139 . then ( ( created ) => navigate ( `/studio/storyboard/${ created . id } ` ) )
130- . finally ( ( ) => setCreating ( null ) ) ;
140+ . finally ( ( ) => {
141+ creatingRef . current = false ;
142+ setCreating ( null ) ;
143+ } ) ;
131144 } , [ createStoryboard , navigate ] ) ;
132145
133146 const startScript = useCallback ( ( ) => {
147+ if ( creatingRef . current ) return ;
148+ creatingRef . current = true ;
134149 setCreating ( "script" ) ;
135150 createScript
136151 . mutateAsync ( { name : "Untitled script" , projectId : "default" } )
137152 . then ( ( created ) => navigate ( `/studio/script/${ created . id } ` ) )
138- . finally ( ( ) => setCreating ( null ) ) ;
153+ . finally ( ( ) => {
154+ creatingRef . current = false ;
155+ setCreating ( null ) ;
156+ } ) ;
139157 } , [ createScript , navigate ] ) ;
140158
141159 return (
@@ -161,6 +179,7 @@ const StudioHome = () => {
161179 description = "Describe your idea. The director agent breaks it into shots, renders stills, animates them into clips, and cuts them together."
162180 cta = "New storyboard"
163181 busy = { creating === "storyboard" }
182+ disabled = { creating !== null }
164183 onStart = { startStoryboard }
165184 />
166185 < PathCard
@@ -169,6 +188,7 @@ const StudioHome = () => {
169188 description = "Write or generate a script, cast a voice for every speaker, and turn the voiced lines into a video with captions."
170189 cta = "New script"
171190 busy = { creating === "script" }
191+ disabled = { creating !== null }
172192 onStart = { startScript }
173193 />
174194 </ FlexRow >
0 commit comments