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;
+};