diff --git a/src/assets/UI/homeBGWeb.png b/src/assets/UI/homeBGWeb.png index 569c477..1d6b866 100644 Binary files a/src/assets/UI/homeBGWeb.png and b/src/assets/UI/homeBGWeb.png differ diff --git a/src/assets/UI/leftIcon.png b/src/assets/UI/leftIcon.png new file mode 100644 index 0000000..38a6903 Binary files /dev/null and b/src/assets/UI/leftIcon.png differ diff --git a/src/assets/UI/rightIcon.png b/src/assets/UI/rightIcon.png new file mode 100644 index 0000000..4a63a90 Binary files /dev/null and b/src/assets/UI/rightIcon.png differ diff --git a/src/assets/index.js b/src/assets/index.js index b56e3b8..7611fc5 100644 --- a/src/assets/index.js +++ b/src/assets/index.js @@ -230,6 +230,8 @@ export const ai = { theo, bena, john, + leftIcon: require("./UI/leftIcon.png"), + rightIcon: require("./UI/rightIcon.png"), }; export const videos = { diff --git a/src/components/BorderGradientButton.js b/src/components/BorderGradientButton.js index 10daa7c..957e5f6 100644 --- a/src/components/BorderGradientButton.js +++ b/src/components/BorderGradientButton.js @@ -55,9 +55,6 @@ const BorderGradientButton = ({ borderRadius: 14, gap: 11, }} - // experimentalBlurMethod={ - // Platform.OS !== "ios" ? "dimezisBlurView" : "none" - // } > {icon && } { + const stageStates = useMemo( + () => getCreationStageStates(selectedProject), + [selectedProject], + ); + + const carouselItems = useMemo( + () => + STAGE_CARD_CONTENT.map((item, index) => ({ + ...item, + isLocked: stageStates[index]?.isLocked ?? true, + })), + [stageStates], + ); + const { height: windowHeight } = useWindowDimensions(); + const itemHeight = Math.max(windowHeight, 1); + + const listRef = useRef(null); + + const keyExtractor = useCallback((item) => item.key, []); + + const renderItem = useCallback( + ({ item }) => , + [], + ); + + const getItemLayout = useCallback( + (_data, index) => ({ + length: itemHeight, + offset: itemHeight * index, + index, + }), + [itemHeight], + ); + + const viewabilityConfig = useRef({ viewAreaCoveragePercentThreshold: 60 }); + + const handleViewableItemsChanged = useCallback( + ({ viewableItems }) => { + if (!onActiveIndexChange) { + return; + } + const firstVisible = viewableItems?.[0]; + if (firstVisible?.index != null) { + onActiveIndexChange(firstVisible.index); + } + }, + [onActiveIndexChange], + ); + + useEffect(() => { + if ( + listRef.current == null || + typeof activeIndex !== "number" || + activeIndex < 0 || + activeIndex >= carouselItems.length + ) { + return; + } + try { + listRef.current.scrollToIndex({ index: activeIndex, animated: true }); + } catch (_error) { + // Ignore scroll errors when list is not ready yet. + } + }, [activeIndex, carouselItems.length]); + + return ( + + + + ); +}; + +export default FeatureCarousel; + +const styles = StyleSheet.create({ + container: { + flex: 1, + width: "100%", + }, + list: { + flex: 1, + }, + listContent: { + flexGrow: 1, + }, + cardRight: { + flexDirection: "row-reverse", + alignItems: "center", + }, +}); diff --git a/src/components/ProjectDropDown/ProjectDropDown.js b/src/components/ProjectDropDown/ProjectDropDown.js index a8c4f3a..903849d 100644 --- a/src/components/ProjectDropDown/ProjectDropDown.js +++ b/src/components/ProjectDropDown/ProjectDropDown.js @@ -124,12 +124,20 @@ const ProjectDropDown = ({ + + + + + ); +} diff --git a/src/components/ShareBtn/ShareBtn.web.js b/src/components/ShareBtn/ShareBtn.web.js new file mode 100644 index 0000000..bbb3468 --- /dev/null +++ b/src/components/ShareBtn/ShareBtn.web.js @@ -0,0 +1,41 @@ +import React from "react"; +import { BlurView } from "expo-blur"; +import { Pressable, Text } from "react-native"; +import { Palette } from "../../styles"; +import { FONT_FAMILY } from "../../styles/Fonts"; +import { Entypo } from "@expo/vector-icons"; + +export default function ShareBtn({ style, onPress = null }) { + return ( + + + + {"Partager l’expérience"} + + + + + ); +} diff --git a/src/components/cards/PersonaCard/PersonaCard.js b/src/components/cards/PersonaCard/PersonaCard.js new file mode 100644 index 0000000..6efcc70 --- /dev/null +++ b/src/components/cards/PersonaCard/PersonaCard.js @@ -0,0 +1,99 @@ +import React from "react"; +import { Text, useWindowDimensions, View } from "react-native"; +import { Image } from "expo-image"; +import { BlurView } from "expo-blur"; +import { gutters, Palette } from "../../../styles"; +import { FONT_FAMILY } from "../../../styles/Fonts"; +import FontAwesome from "@expo/vector-icons/FontAwesome"; +import palette from "../../../styles/Palette"; + +export default function PersonaCard({ item, isLock = false }) { + const { height: windowHeight } = useWindowDimensions(); + + return ( + + + + + + {item.title} + + + {item.description} + + {isLock && ( + + + + + + )} + + + + ); +} diff --git a/src/components/cards/PersonaCard/PersonaCard.web.js b/src/components/cards/PersonaCard/PersonaCard.web.js new file mode 100644 index 0000000..30704c5 --- /dev/null +++ b/src/components/cards/PersonaCard/PersonaCard.web.js @@ -0,0 +1,111 @@ +import React from "react"; +import { Text, useWindowDimensions, View } from "react-native"; +import { Image } from "expo-image"; +import { BlurView } from "expo-blur"; +import { Palette } from "../../../styles"; +import { FONT_FAMILY } from "../../../styles/Fonts"; +import FontAwesome from "@expo/vector-icons/FontAwesome"; +import palette from "../../../styles/Palette"; + +export default function PersonaCard({ item, index, isLock = true }) { + const isImageOnLeft = index % 2 === 0; + const { height: windowHeight } = useWindowDimensions(); + + return ( + + + + + + + {item.title} + + + {item.description} + + {isLock && ( + + + + + + )} + + + + + ); +} diff --git a/src/layouts/Page.js b/src/layouts/Page.js index f97e949..b29627a 100644 --- a/src/layouts/Page.js +++ b/src/layouts/Page.js @@ -3,14 +3,12 @@ import { Image, View } from "react-native"; import { KeyboardAwareScrollView } from "react-native-keyboard-aware-scroll-view"; import { SafeAreaView } from "react-native-safe-area-context"; import React from "reactn"; - import { responsiveHeight } from "../actions/responsiveSizes.js"; import BaseHeader from "../components/BaseHeader"; import NavigateHeader from "../components/NavigateHeader"; import { gutters } from "../styles"; - import { isWeb } from "../hooks/useLayoutType.js"; -// import { useUserData } from "../providers/UserDataProvider.js"; +import ShareBtn from "../components/ShareBtn/ShareBtn"; export default ({ children, @@ -35,10 +33,9 @@ export default ({ headerTitleStyle = {}, hideBackButton = false, width = undefined, - maxWidth = null, + maxWidth = 1200, + shareBtn = false, }) => { - // const { currentUID } = useUserData(); - const PageContainer = containerType === "SAFE_AREA_VIEW" ? SafeAreaView : View; @@ -112,6 +109,15 @@ export default ({ {bottomStickyContent?.()} + {shareBtn && isWeb && ( + + )} ); }; diff --git a/src/providers/UserDataProvider.js b/src/providers/UserDataProvider.js index 5ad6957..cf55a0d 100644 --- a/src/providers/UserDataProvider.js +++ b/src/providers/UserDataProvider.js @@ -114,6 +114,7 @@ export default ({ children }) => { }, { merge: true }, ); + console.log("update selected project"); } catch (error) { console.log( "UserDataProvider: unable to persist selectedProjectId", @@ -158,7 +159,7 @@ export default ({ children }) => { ...partial, updatedAt: firebase.firestore.FieldValue.serverTimestamp(), }, - options + options, ); } catch (e) { console.log("updateProjectData error", e?.message); @@ -194,7 +195,7 @@ export default ({ children }) => { followedBy: arrayUnion(currentUID), lastFollowersUpdateAt: new Date(), }, - { merge: true } + { merge: true }, ); setTooltip({ type: "success", text: "Abonnement mis à jour" }); } @@ -212,7 +213,7 @@ export default ({ children }) => { followedBy: arrayRemove(currentUID), lastFollowersUpdateAt: new Date(), }, - { merge: true } + { merge: true }, ); setTooltip({ type: "success", @@ -345,7 +346,11 @@ export default ({ children }) => { if (remoteSelectedId !== selectedProjectId) { setSelectedProjectId(remoteSelectedId); } - }, [currentUserDoc?.selectedProjectId, selectedProjectId, setSelectedProject]); + }, [ + currentUserDoc?.selectedProjectId, + selectedProjectId, + setSelectedProject, + ]); useEffect(() => { if (!Array.isArray(userProjects) || userProjects.length === 0) return; diff --git a/src/screens/Home/Home.js b/src/screens/Home/Home.js index a2abc35..72ec7a4 100644 --- a/src/screens/Home/Home.js +++ b/src/screens/Home/Home.js @@ -1,145 +1,188 @@ -import { View, Text, Image, Platform } from "react-native"; -import React, { useMemo, useState } from "react"; +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import { Platform, StyleSheet, View } from "react-native"; import Page from "../../layouts/Page"; -import { background, img } from "../../assets"; -import { responsiveHeight } from "react-native-responsive-dimensions"; -import { Palette } from "../../styles"; -import { FONT_FAMILY } from "../../styles/Fonts"; +import { background } from "../../assets"; +import { gutters, Palette } from "../../styles"; import { navigate } from "../../navigation/NavigationService"; import { Routes } from "../../navigation"; -import { FlatList, Alert } from "react-native"; -import { BlurView } from "expo-blur"; import { useUser } from "../../providers/UserDataProvider"; -import MusicCard from "../Library/components/MusicCard"; +import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown"; +import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel"; import GradientButton from "../../components/GradientButton"; -import MoreMenu from "../../components/MoreMenu"; -import { projectsRef } from "../../config/firebase"; -import { useGlobal } from "reactn"; +import BorderGradientButton from "../../components/BorderGradientButton"; +import { isWeb } from "../../hooks/useLayoutType.js"; +import ShareBtn from "../../components/ShareBtn/ShareBtn"; +import { + getCreationStageStates, + getStageAction, +} from "../../utils/projectStages"; const Home = () => { - const { userProjects = [], resetSelectedProject, selectProject } = useUser(); + const { + userProjects = [], + resetSelectedProject, + selectProject, + selectedProject, + selectedProjectId, + } = useUser(); + const projects = useMemo( () => (Array.isArray(userProjects) ? userProjects : []), [userProjects], ); - const [, setTooltip] = useGlobal("_tooltip"); - const [menuTop, setMenuTop] = useState(0); - const [showMenu, setShowMenu] = useState(false); - const [menuProjectId, setMenuProjectId] = useState(null); + const currentProject = useMemo(() => { + if (!projects.length) return null; + const activeId = selectedProject?.id || selectedProjectId; + if (!activeId) { + return projects[0]; + } + return projects.find((project) => project?.id === activeId) || projects[0]; + }, [projects, selectedProject?.id, selectedProjectId]); + + const formatDate = useCallback((timestamp) => { + try { + const value = timestamp?.toDate ? timestamp.toDate() : timestamp; + const date = value ? new Date(value) : null; + if (!date || Number.isNaN(date.getTime())) { + return ""; + } + const day = date.getDate().toString().padStart(2, "0"); + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + return `${day}/${month}`; + } catch (error) { + console.warn("Home.web: formatDate error", error); + return ""; + } + }, []); + + const handleSelectProject = (project) => { + if (!project?.id) return; + selectProject(project.id); + }; + + const handleModifyProject = (project) => { + if (!project?.id) return; + selectProject(project.id); + navigate(Routes.FlowSelection); + }; + + const handleCreateNew = () => { + resetSelectedProject(); + navigate(Routes.FlowSelection); + }; + + const stageStates = useMemo( + () => getCreationStageStates(currentProject), + [currentProject], + ); + + const firstUnlockedIndex = useMemo(() => { + const index = stageStates.findIndex((stage) => !stage.isLocked); + return index === -1 ? 0 : index; + }, [stageStates]); + + const [activeStageIndex, setActiveStageIndex] = useState(firstUnlockedIndex); + + useEffect(() => { + setActiveStageIndex((prev) => { + if (prev == null || prev >= stageStates.length) { + return firstUnlockedIndex; + } + const current = stageStates[prev]; + const fallback = stageStates[firstUnlockedIndex]; + if (current?.isLocked && fallback && !fallback.isLocked) { + return firstUnlockedIndex; + } + return prev; + }); + }, [stageStates, firstUnlockedIndex]); + + const activeStage = + stageStates[activeStageIndex] || stageStates[firstUnlockedIndex]; + const stageAction = useMemo(() => { + if (!activeStage) { + return null; + } + return getStageAction(activeStage.key, currentProject); + }, [activeStage, currentProject]); + + const stageRoute = stageAction?.route || null; + const stageParams = stageAction?.params; + const stageLocked = activeStage?.isLocked ?? true; + + const ensureProjectSelected = useCallback(() => { + if (!currentProject?.id) { + return; + } + if (selectedProject?.id !== currentProject.id) { + selectProject(currentProject.id); + } + }, [currentProject?.id, selectProject, selectedProject?.id]); + + const handleStageAction = useCallback(() => { + if (stageLocked || !stageRoute) { + return; + } + ensureProjectSelected(); + navigate(stageRoute, stageParams); + }, [ensureProjectSelected, stageLocked, stageParams, stageRoute]); + + const primaryDisabled = stageLocked || !stageRoute; return ( - - - - {projects.length > 0 && ( - - - - Musiques en cours - - item.id} - contentContainerStyle={{ gap: 10, paddingBottom: 10 }} - renderItem={({ item }) => ( - { - selectProject(item.id); - navigate(Routes.FlowSelection); - }} - onPressMore={(posTop) => { - setMenuProjectId(item.id); - setMenuTop(posTop); - setShowMenu((prev) => !prev || posTop !== menuTop); - }} - /> - )} - /> - setShowMenu(false)} - inPlaylist={false} - projectId={menuProjectId} - extraItems={[ - { - label: "Supprimer", - onPress: () => - Alert.alert( - "Confirmer la suppression", - "Cette action supprimera définitivement ce projet.", - [ - { text: "Annuler", style: "cancel" }, - { - text: "Supprimer", - style: "destructive", - onPress: async () => { - try { - if (!menuProjectId) return; - await projectsRef.doc(menuProjectId).delete(); - setTooltip({ - type: "success", - text: "Projet supprimé", - }); - } catch (e) { - setTooltip({ - type: "error", - text: e?.message || "Suppression impossible", - }); - } - }, - }, - ], - { cancelable: true }, - ), - }, - ]} - /> - - - )} - + + + + + {!isWeb && } + + + + + { - resetSelectedProject(); - navigate(Routes.FlowSelection); - }} + containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }} + title={"Commencer à créer"} + onPress={handleStageAction} + disabled={primaryDisabled} + /> + @@ -148,3 +191,27 @@ const Home = () => { }; export default Home; + +const styles = StyleSheet.create({ + root: { + flex: 1, + }, + heroImage: { + position: "absolute", + top: -60, + alignSelf: "center", + width: "80%", + maxWidth: 920, + height: 420, + opacity: 0.9, + }, + shareIcon: { + width: 18, + height: 18, + tintColor: Palette.white, + }, + carouselSection: { + width: "100%", + flex: 1, + }, +}); diff --git a/src/screens/Home/Home.web.js b/src/screens/Home/Home.web.js deleted file mode 100644 index 6841bca..0000000 --- a/src/screens/Home/Home.web.js +++ /dev/null @@ -1,333 +0,0 @@ -import React, { useCallback, useMemo } from "react"; -import { - FlatList, - Image, - StyleSheet, - Text, - View, - useWindowDimensions, -} from "react-native"; -import { BlurView } from "expo-blur"; -import Page from "../../layouts/Page"; -import { background, img } from "../../assets"; -import { Palette } from "../../styles"; -import { FONT_FAMILY } from "../../styles/Fonts"; -import { navigate } from "../../navigation/NavigationService"; -import { Routes } from "../../navigation"; -import { useUser } from "../../providers/UserDataProvider"; -import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown"; - -const ITEM_SPACING = 24; - -const Home = () => { - const { - userProjects = [], - resetSelectedProject, - selectProject, - selectedProject, - selectedProjectId, - } = useUser(); - - const projects = useMemo( - () => (Array.isArray(userProjects) ? userProjects : []), - [userProjects], - ); - - const { width: windowWidth } = useWindowDimensions(); - - const carouselItems = useMemo( - () => [ - { - id: "project-vision", - title: "Composez sans limites", - description: - "Créez instantanément des maquettes professionnelles et explorez de nouveaux genres.", - image: img.placeholder, - }, - { - id: "project-community", - title: "Collaborez en équipe", - description: - "Partagez vos projets, échangez des idées et co-créez en temps réel.", - image: img.placeholder2, - }, - { - id: "project-ai", - title: "Optimisé par l'IA", - description: - "Accédez à des suggestions intelligentes pour les paroles, arrangements et mixages.", - image: img.placeholder3, - }, - { - id: "project-stage", - title: "Prêt pour la scène", - description: - "Finalisez vos titres et exportez-les facilement pour le live ou le streaming.", - image: img.placeholder4, - }, - ], - [], - ); - - const carouselItemWidth = useMemo(() => { - const baseWidth = Math.min(windowWidth * 0.9, 640); - return Math.max(baseWidth, 320); - }, [windowWidth]); - - const carouselItemHeight = useMemo(() => { - const baseHeight = Math.min(windowWidth * 0.6, 360); - return Math.max(baseHeight, 240); - }, [windowWidth]); - - const snapInterval = useMemo( - () => carouselItemHeight + ITEM_SPACING, - [carouselItemHeight], - ); - - const currentProject = useMemo(() => { - if (!projects.length) return null; - const activeId = selectedProject?.id || selectedProjectId; - if (!activeId) { - return projects[0]; - } - return projects.find((project) => project?.id === activeId) || projects[0]; - }, [projects, selectedProject?.id, selectedProjectId]); - - const formatDate = useCallback((timestamp) => { - try { - const value = timestamp?.toDate ? timestamp.toDate() : timestamp; - const date = value ? new Date(value) : null; - if (!date || Number.isNaN(date.getTime())) { - return ""; - } - const day = date.getDate().toString().padStart(2, "0"); - const month = (date.getMonth() + 1).toString().padStart(2, "0"); - return `${day}/${month}`; - } catch (error) { - console.warn("Home.web: formatDate error", error); - return ""; - } - }, []); - - const handleSelectProject = (project) => { - if (!project?.id) return; - selectProject(project.id); - }; - - const handleModifyProject = (project) => { - if (!project?.id) return; - selectProject(project.id); - navigate(Routes.FlowSelection); - }; - - const handleCreateNew = () => { - resetSelectedProject(); - navigate(Routes.FlowSelection); - }; - - const renderCarouselItem = useCallback( - ({ item, index }) => { - const isImageOnRight = index % 2 === 1; - - let imageSize = Math.min(carouselItemHeight - 24, 280); - let availableWidth = carouselItemWidth - imageSize - 48; - - if (availableWidth < 140) { - const minImageSize = Math.max(carouselItemWidth - 140 - 48, 140); - imageSize = Math.min(imageSize, minImageSize); - availableWidth = carouselItemWidth - imageSize - 48; - } - - const blurWidth = Math.min(Math.max(availableWidth, 120), 260); - const blurHeight = Math.max( - Math.min(imageSize * 0.75, carouselItemHeight - 48), - 140, - ); - - return ( - - - - - {item.title} - {item.description} - - - - ); - }, - [carouselItemHeight, carouselItemWidth], - ); - - const keyExtractor = useCallback((item) => item.id, []); - - return ( - - - - - - - ( - - )} - /> - - - - ); -}; - -export default Home; - -const styles = StyleSheet.create({ - root: { - flex: 1, - paddingHorizontal: 24, - position: "relative", - justifyContent: "flex-start", - alignItems: "center", - width: "100%", - }, - heroImage: { - position: "absolute", - top: -60, - alignSelf: "center", - width: "80%", - maxWidth: 920, - height: 420, - opacity: 0.9, - }, - dropdownArea: { - width: "100%", - alignItems: "center", - justifyContent: "center", - paddingTop: 72, - zIndex: 2, - }, - dropdownContainer: { - width: "100%", - }, - shareIcon: { - width: 18, - height: 18, - tintColor: Palette.white, - }, - carouselSection: { - width: "100%", - marginTop: 48, - }, - carouselList: { - width: "100%", - }, - carouselItem: { - borderRadius: 18, - backgroundColor: "rgba(0, 0, 0, 0.18)", - borderWidth: 1, - borderColor: "rgba(255, 255, 255, 0.1)", - paddingVertical: 24, - paddingHorizontal: 16, - alignItems: "center", - justifyContent: "center", - }, - carouselItemRight: { - flexDirection: "row-reverse", - alignItems: "center", - }, - carouselItemLeft: { - flexDirection: "row", - alignItems: "center", - }, - carouselImage: { - borderRadius: 28, - shadowColor: "#000", - shadowOffset: { width: 0, height: 8 }, - shadowOpacity: 0.25, - shadowRadius: 20, - }, - carouselImageRight: { - marginLeft: 20, - }, - carouselImageLeft: { - marginRight: 20, - }, - carouselBlur: { - borderRadius: 20, - overflow: "hidden", - paddingHorizontal: 18, - paddingVertical: 16, - justifyContent: "center", - alignItems: "flex-start", - backgroundColor: "rgba(0, 0, 0, 0.25)", - gap: 8, - }, - carouselBlurRight: { - marginRight: 12, - }, - carouselBlurLeft: { - marginLeft: 12, - }, - carouselTextContainer: { - width: "100%", - }, - carouselTitle: { - fontSize: 20, - color: Palette.white, - fontFamily: FONT_FAMILY.InterSemiBold, - marginBottom: 8, - }, - carouselDescription: { - fontSize: 14, - lineHeight: 20, - color: "rgba(255, 255, 255, 0.7)", - fontFamily: FONT_FAMILY.InterRegular, - }, -}); diff --git a/src/screens/Home/HomeSave.js b/src/screens/Home/HomeSave.js new file mode 100644 index 0000000..2fe0c31 --- /dev/null +++ b/src/screens/Home/HomeSave.js @@ -0,0 +1,150 @@ +import { View, Text, Image, Platform } from "react-native"; +import React, { useMemo, useState } from "react"; +import Page from "../../layouts/Page"; +import { background, img } from "../../assets"; +import { responsiveHeight } from "react-native-responsive-dimensions"; +import { Palette } from "../../styles"; +import { FONT_FAMILY } from "../../styles/Fonts"; +import { navigate } from "../../navigation/NavigationService"; +import { Routes } from "../../navigation"; +import { FlatList, Alert } from "react-native"; +import { BlurView } from "expo-blur"; +import { useUser } from "../../providers/UserDataProvider"; +import MusicCard from "../Library/components/MusicCard"; +import GradientButton from "../../components/GradientButton"; +import MoreMenu from "../../components/MoreMenu"; +import { projectsRef } from "../../config/firebase"; +import { useGlobal } from "reactn"; + +const HomeSave = () => { + const { userProjects = [], resetSelectedProject, selectProject } = useUser(); + const projects = useMemo( + () => (Array.isArray(userProjects) ? userProjects : []), + [userProjects], + ); + const [, setTooltip] = useGlobal("_tooltip"); + const [menuTop, setMenuTop] = useState(0); + const [showMenu, setShowMenu] = useState(false); + const [menuProjectId, setMenuProjectId] = useState(null); + + return ( + + + + {projects.length > 0 && ( + + + + Musiques en cours + + item.id} + contentContainerStyle={{ gap: 10, paddingBottom: 10 }} + renderItem={({ item }) => ( + { + selectProject(item.id); + navigate(Routes.FlowSelection); + }} + onPressMore={(posTop) => { + setMenuProjectId(item.id); + setMenuTop(posTop); + setShowMenu((prev) => !prev || posTop !== menuTop); + }} + /> + )} + /> + setShowMenu(false)} + inPlaylist={false} + projectId={menuProjectId} + extraItems={[ + { + label: "Supprimer", + onPress: () => + Alert.alert( + "Confirmer la suppression", + "Cette action supprimera définitivement ce projet.", + [ + { text: "Annuler", style: "cancel" }, + { + text: "Supprimer", + style: "destructive", + onPress: async () => { + try { + if (!menuProjectId) return; + await projectsRef.doc(menuProjectId).delete(); + setTooltip({ + type: "success", + text: "Projet supprimé", + }); + } catch (e) { + setTooltip({ + type: "error", + text: e?.message || "Suppression impossible", + }); + } + }, + }, + ], + { cancelable: true }, + ), + }, + ]} + /> + + + )} + + { + resetSelectedProject(); + navigate(Routes.FlowSelection); + }} + /> + + + + ); +}; + +export default Home; diff --git a/src/screens/NewMusicOptions.js b/src/screens/NewMusicOptions.js index 87d15c7..aeeadcf 100644 --- a/src/screens/NewMusicOptions.js +++ b/src/screens/NewMusicOptions.js @@ -1,18 +1,19 @@ import FontAwesome from "@expo/vector-icons/FontAwesome"; import { BlurView } from "expo-blur"; -import React from "react"; +import React, { useCallback, useMemo } from "react"; import { Image, Platform, Pressable, Text, View } from "react-native"; import { ai, background } from "../assets"; import Page from "../layouts/Page"; -import { Routes } from "../navigation"; import { navigate } from "../navigation/NavigationService"; import { useUserData } from "../providers/UserDataProvider"; +import { getCreationStageStates, getStageAction } from "../utils/projectStages"; import { Palette } from "../styles"; import { FONT_FAMILY } from "../styles/Fonts"; import palette from "../styles/Palette"; const CREATE_DATA = [ { + stageKey: "songwriter", img: ai.nathalie, bg: background.writingBG, label: "Nathalie", @@ -20,6 +21,7 @@ const CREATE_DATA = [ type: "Songwriter", }, { + stageKey: "beatmaker", img: ai.theo, bg: background.studioBG, label: "Theo", @@ -27,6 +29,7 @@ const CREATE_DATA = [ type: "Beatmaker", }, { + stageKey: "designer", img: ai.bena, bg: background.productionBG, label: "Bena", @@ -34,6 +37,7 @@ const CREATE_DATA = [ type: "Designer", }, { + stageKey: "director", img: ai.john, bg: background.playbackBG, label: "John", @@ -44,69 +48,32 @@ const CREATE_DATA = [ const NewMusicOptions = () => { const { selectedProject } = useUserData(); + const stageStates = useMemo( + () => getCreationStageStates(selectedProject), + [selectedProject], + ); - // Lock rules per option index: - // 0 (Songwriter): allowed if no project OR no songUrl - // 1 (Beatmaker): allowed only if lyrics exist and no songUrl - // 2 (Designer): allowed only if songUrl exists and no coverUrl - // 3 (Director): allowed only if coverUrl exists - const isLocked = (index) => { - const songUrl = selectedProject?.songUrl || null; - const coverUrl = selectedProject?.coverUrl || null; - const playbackUrl = selectedProject?.songUrl || null; - const lyricsLen = Array.isArray(selectedProject?.lyrics) - ? selectedProject.lyrics.length - : 0; + const stageStateByKey = useMemo(() => { + return stageStates.reduce((acc, stage) => { + acc[stage.key] = stage; + return acc; + }, {}); + }, [stageStates]); - switch (index) { - case 0: // Songwriter - return !!songUrl; // locked if a song already exists - case 1: // Beatmaker - return !(lyricsLen > 0 && !songUrl); - case 2: // Designer - return !(!!songUrl && !!playbackUrl); - case 3: // Director - return !!!coverUrl; - default: - return true; - } - }; - - const onPressOption = (index, item) => { - if (isLocked(index)) return; - switch (index) { - case 0: - if (selectedProject?.lyrics?.length) { - navigate(Routes.Lyrics); - } else { - navigate(Routes.WritingLyrics); - } - break; - case 1: - if (selectedProject?.musicStatus === "GENERATING") { - navigate(Routes.GeneratingSong); - } else if (selectedProject?.musicStatus === "GENERATED") { - navigate(Routes.SongReady); - } else { - navigate(Routes.Compose); - } - break; - case 2: - if (selectedProject?.coverUrl) { - navigate(Routes.ValidateCover); - } else { - navigate(Routes.ChooseCoverType); - } - break; - case 3: - console.log("test"); - navigate(Routes.Playback, { - project: selectedProject, - }); - default: - break; - } - }; + const onPressOption = useCallback( + (stageKey) => { + const stageState = stageStateByKey[stageKey]; + if (!stageState || stageState.isLocked) { + return; + } + const action = getStageAction(stageKey, selectedProject); + if (!action?.route) { + return; + } + navigate(action.route, action.params); + }, + [stageStateByKey, selectedProject], + ); return ( { scrollEnabled={true} > - {CREATE_DATA.map((item, index) => { - const locked = isLocked(index); + {CREATE_DATA.map((item) => { + const locked = stageStateByKey[item.stageKey]?.isLocked ?? true; return ( - onPressOption(index, item)}> + onPressOption(item.stageKey)} + > { + if (!project) return 0; + const { lyrics } = project; + return Array.isArray(lyrics) ? lyrics.length : 0; +}; + +const getStageLockState = (key, project) => { + const hasSong = !!project?.songUrl; + const hasCover = !!project?.coverUrl; + const hasPlayback = !!(project?.playbackUrl || project?.songUrl); + const lyricsCount = getLyricsCount(project); + + switch (key) { + case "songwriter": + return hasSong; + case "beatmaker": + return !(lyricsCount > 0 && !hasSong); + case "designer": + return !(hasSong && hasPlayback); + case "director": + return !hasCover; + default: + return true; + } +}; + +export const getCreationStageStates = (project) => + CREATION_STAGE_KEYS.map((key, index) => ({ + key, + index, + isLocked: getStageLockState(key, project), + })); + +export const getStageAction = (key, project) => { + switch (key) { + case "songwriter": + return { + route: + getLyricsCount(project) > 0 ? Routes.Lyrics : Routes.WritingLyrics, + }; + case "beatmaker": + if (project?.musicStatus === "GENERATING") { + return { route: Routes.GeneratingSong }; + } + if (project?.musicStatus === "GENERATED") { + return { route: Routes.SongReady }; + } + return { route: Routes.Compose }; + case "designer": + return { + route: project?.coverUrl + ? Routes.ValidateCover + : Routes.ChooseCoverType, + }; + case "director": + return { + route: Routes.Playback, + params: project ? { project } : undefined, + }; + default: + return {}; + } +}; + +export const findFirstUnlockedStageIndex = (project) => { + const stages = getCreationStageStates(project); + const index = stages.findIndex((stage) => !stage.isLocked); + return index === -1 ? 0 : index; +};