diff --git a/src/components/FeatureCarousel/FeatureCarousel.js b/src/components/FeatureCarousel/FeatureCarousel.js index 6873d4e..081c5ef 100644 --- a/src/components/FeatureCarousel/FeatureCarousel.js +++ b/src/components/FeatureCarousel/FeatureCarousel.js @@ -9,38 +9,41 @@ const STAGE_CARD_CONTENT = [ key: "songwriter", title: "Nathalie", description: "Let’s write lyrics together !", - image: ai.nathalie, + image: ai.leftIcon, }, { key: "beatmaker", title: "Theo", description: "Come back, when you'll have lyrics!", - image: ai.theo, + image: ai.rightIcon, }, { key: "designer", title: "Bena", description: "Come back when you want to generate cover!", - image: ai.bena, + image: ai.leftIcon, }, { key: "director", title: "John", description: "Come back when your audio is ready!", - image: ai.john, + image: ai.rightIcon, }, ]; const FeatureCarousel = ({ style, selectedProject, + stageStates: stageStatesProp, activeIndex, onActiveIndexChange, }) => { - const stageStates = useMemo( - () => getCreationStageStates(selectedProject), - [selectedProject], - ); + const stageStates = useMemo(() => { + if (stageStatesProp) { + return stageStatesProp; + } + return getCreationStageStates(selectedProject); + }, [stageStatesProp, selectedProject]); const carouselItems = useMemo( () => @@ -54,11 +57,24 @@ const FeatureCarousel = ({ const itemHeight = Math.max(windowHeight, 1); const listRef = useRef(null); + const pendingScrollRef = useRef(false); + const activeIndexRef = useRef(activeIndex); + const onActiveIndexChangeRef = useRef(onActiveIndexChange); + + useEffect(() => { + activeIndexRef.current = activeIndex; + }, [activeIndex]); + + useEffect(() => { + onActiveIndexChangeRef.current = onActiveIndexChange; + }, [onActiveIndexChange]); const keyExtractor = useCallback((item) => item.key, []); const renderItem = useCallback( - ({ item }) => , + ({ item, index }) => ( + + ), [], ); @@ -73,18 +89,24 @@ const FeatureCarousel = ({ const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 }); - const handleViewableItemsChanged = useCallback( - ({ viewableItems }) => { - if (!onActiveIndexChange) { + const handleViewableItemsChangedRef = useRef(); + if (!handleViewableItemsChangedRef.current) { + handleViewableItemsChangedRef.current = ({ viewableItems }) => { + if (!viewableItems?.length) return; + const firstVisible = viewableItems.find((item) => item?.isViewable); + if (!firstVisible || firstVisible.index == null) return; + if (pendingScrollRef.current) { + if (firstVisible.index === activeIndexRef.current) { + pendingScrollRef.current = false; + } return; } - const firstVisible = viewableItems?.[0]; - if (firstVisible?.index != null) { - onActiveIndexChange(firstVisible.index); + const callback = onActiveIndexChangeRef.current; + if (callback && firstVisible.index !== activeIndexRef.current) { + callback(firstVisible.index); } - }, - [onActiveIndexChange], - ); + }; + } useEffect(() => { if ( @@ -96,8 +118,10 @@ const FeatureCarousel = ({ return; } try { + pendingScrollRef.current = true; listRef.current.scrollToIndex({ index: activeIndex, animated: true }); } catch (_error) { + pendingScrollRef.current = false; // Ignore scroll errors when list is not ready yet. } }, [activeIndex, carouselItems.length]); @@ -109,7 +133,7 @@ const FeatureCarousel = ({ data={carouselItems} keyExtractor={keyExtractor} renderItem={renderItem} - onViewableItemsChanged={handleViewableItemsChanged} + onViewableItemsChanged={handleViewableItemsChangedRef.current} viewabilityConfig={viewabilityConfig.current} getItemLayout={getItemLayout} showsVerticalScrollIndicator={false} diff --git a/src/components/ProjectDropDown/ProjectDropDown.js b/src/components/ProjectDropDown/ProjectDropDown.js index 903849d..d3b2008 100644 --- a/src/components/ProjectDropDown/ProjectDropDown.js +++ b/src/components/ProjectDropDown/ProjectDropDown.js @@ -41,6 +41,7 @@ const ProjectDropDown = ({ onModifyProject, onCreateProject, formatDate = defaultFormatDate, + allowEmptySelection = false, }) => { const [isOpen, setIsOpen] = useState(false); const [triggerLayout, setTriggerLayout] = useState(null); @@ -48,9 +49,14 @@ const ProjectDropDown = ({ const normalizedProjects = Array.isArray(projects) ? projects : []; const isDisabled = normalizedProjects.length === 0; - const currentProject = selectedProject?.id - ? selectedProject - : normalizedProjects[0] || null; + const hasExplicitNullSelection = + allowEmptySelection && selectedProject === null; + + const currentProject = hasExplicitNullSelection + ? null + : selectedProject?.id + ? selectedProject + : normalizedProjects[0] || null; const dropdownProjects = currentProject ? normalizedProjects.filter((project) => { @@ -212,19 +218,28 @@ const ProjectRow = ({ onModify?.(project, top); }); return; - } catch (error) { + } catch (_error) { // Fallback to layout-derived position } } onModify?.(project, menuPos); }; + const isEmptyProject = !project; const coverUri = project?.coverUrl || project?.coverUri || (typeof project?.cover === "string" ? project.cover : project?.cover?.uri); - const title = project?.title || "Sans titre"; - const formattedDate = formatDate(project?.updatedAt || project?.createdAt); + const title = + project?.title || (isEmptyProject ? "Nouvelle musique" : "Sans titre"); + const formattedDate = !isEmptyProject + ? formatDate(project?.updatedAt || project?.createdAt) + : ""; + const subtitle = isEmptyProject + ? "Commencer une nouvelle musique" + : formattedDate + ? `Modifié le ${formattedDate}` + : "MusicLand"; return ( - {formattedDate ? `Modifié le ${formattedDate}` : "MusicLand"} + {subtitle} {isTitle ? ( diff --git a/src/providers/UserDataProvider.js b/src/providers/UserDataProvider.js index cf55a0d..d00374a 100644 --- a/src/providers/UserDataProvider.js +++ b/src/providers/UserDataProvider.js @@ -355,7 +355,9 @@ export default ({ children }) => { useEffect(() => { if (!Array.isArray(userProjects) || userProjects.length === 0) return; if (selectedProjectId) return; - if (currentUserDoc?.selectedProjectId) return; + + const remoteSelectedId = currentUserDoc?.selectedProjectId; + if (remoteSelectedId !== undefined) return; const fallbackProjectId = userProjects[0]?.id; if (!fallbackProjectId) return; diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index 72ec7a4..f661e2c 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -15,6 +15,7 @@ import ShareBtn from "../../components/ShareBtn/ShareBtn"; import { getCreationStageStates, getStageAction, + findFirstUnlockedStageIndex, } from "../../utils/projectStages"; const Home = () => { @@ -33,6 +34,9 @@ const Home = () => { const currentProject = useMemo(() => { if (!projects.length) return null; + if (selectedProjectId === null) { + return null; + } const activeId = selectedProject?.id || selectedProjectId; if (!activeId) { return projects[0]; @@ -59,16 +63,13 @@ const Home = () => { const handleSelectProject = (project) => { if (!project?.id) return; selectProject(project.id); + setActiveStageIndex(findFirstUnlockedStageIndex(project)); }; const handleModifyProject = (project) => { if (!project?.id) return; selectProject(project.id); - navigate(Routes.FlowSelection); - }; - - const handleCreateNew = () => { - resetSelectedProject(); + setActiveStageIndex(findFirstUnlockedStageIndex(project)); navigate(Routes.FlowSelection); }; @@ -111,6 +112,8 @@ const Home = () => { const stageParams = stageAction?.params; const stageLocked = activeStage?.isLocked ?? true; + const hasActiveProject = !!currentProject; + const ensureProjectSelected = useCallback(() => { if (!currentProject?.id) { return; @@ -121,14 +124,34 @@ const Home = () => { }, [currentProject?.id, selectProject, selectedProject?.id]); const handleStageAction = useCallback(() => { - if (stageLocked || !stageRoute) { + if (!hasActiveProject || stageLocked || !stageRoute) { return; } ensureProjectSelected(); navigate(stageRoute, stageParams); - }, [ensureProjectSelected, stageLocked, stageParams, stageRoute]); + }, [ + ensureProjectSelected, + hasActiveProject, + stageLocked, + stageParams, + stageRoute, + ]); - const primaryDisabled = stageLocked || !stageRoute; + const songwriterAction = useMemo( + () => getStageAction("songwriter", null), + [], + ); + + const handleStartNew = useCallback(() => { + resetSelectedProject(); + setActiveStageIndex(0); + if (songwriterAction?.route) { + navigate(songwriterAction.route, songwriterAction.params); + } + }, [resetSelectedProject, songwriterAction]); + + const continueDisabled = !hasActiveProject || stageLocked || !stageRoute; + const startDisabled = !songwriterAction?.route; return ( @@ -149,9 +172,10 @@ const Home = () => { style={[isWeb ? { width: "100%" } : { flex: 1 }]} projects={projects} selectedProject={currentProject} + allowEmptySelection onSelectProject={handleSelectProject} onModifyProject={handleModifyProject} - onCreateProject={handleCreateNew} + onCreateProject={handleStartNew} formatDate={formatDate} /> {!isWeb && } @@ -159,6 +183,7 @@ const Home = () => { @@ -175,14 +200,14 @@ const Home = () => {