578 lines
16 KiB
JavaScript
578 lines
16 KiB
JavaScript
import React, {
|
|
useCallback,
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
} from "react";
|
|
import { Platform, StyleSheet, View } from "react-native";
|
|
import { useIsFocused } from "@react-navigation/native";
|
|
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
|
|
import { background, videos } from "../../assets";
|
|
import Alert from "../../components/Alert";
|
|
import BorderGradientButton from "../../components/BorderGradientButton";
|
|
import FeatureCarousel from "../../components/FeatureCarousel/FeatureCarousel";
|
|
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
|
|
import GradientButton from "../../components/GradientButton";
|
|
import MoreMenu from "../../components/MoreMenu";
|
|
import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
|
|
import ShareBtn from "../../components/ShareBtn/ShareBtn";
|
|
import { projectsRef, usersRef, videosRef } from "../../config/firebase";
|
|
import useDataFromRef from "../../hooks/useDataFromRef";
|
|
import { isWeb } from "../../hooks/useLayoutType.js";
|
|
import Page from "../../layouts/Page";
|
|
import LandingPage from "../LandingPage";
|
|
import { navigate } from "../../navigation/NavigationService";
|
|
import { Routes } from "../../navigation/Routes";
|
|
import { useUser } from "../../providers/UserDataProvider";
|
|
import { Palette } from "../../styles";
|
|
import {
|
|
findFirstUnlockedStageIndex,
|
|
getCreationStageStates,
|
|
getStageAction,
|
|
} from "../../utils/projectStages";
|
|
|
|
const isProjectEmpty = (project) => {
|
|
if (!project) {
|
|
return false;
|
|
}
|
|
|
|
return !(
|
|
typeof project?.title === "string" && project.title.trim().length > 0
|
|
);
|
|
};
|
|
|
|
const Home = ({ navigation, route }) => {
|
|
const isFocused = useIsFocused();
|
|
const {
|
|
userProjects = [],
|
|
selectProject,
|
|
selectedProject,
|
|
selectedProjectId,
|
|
currentUserData,
|
|
currentUID,
|
|
createNewProject,
|
|
} = useUser();
|
|
const { setTooltip } = useMinuit();
|
|
|
|
if (!currentUID) {
|
|
return <LandingPage />;
|
|
}
|
|
|
|
const projects = useMemo(
|
|
() => (Array.isArray(userProjects) ? userProjects : []),
|
|
[userProjects]
|
|
);
|
|
|
|
const currentProject = useMemo(() => {
|
|
if (!projects.length) return null;
|
|
if (selectedProjectId === null) {
|
|
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 stageStates = useMemo(
|
|
() => getCreationStageStates(currentProject),
|
|
[currentProject]
|
|
);
|
|
|
|
const preferredStageIndex = useMemo(() => {
|
|
if (!stageStates.length) {
|
|
return 0;
|
|
}
|
|
const actionableIndex = stageStates.findIndex(
|
|
(stage) => !stage.isCompleted && !stage.isLocked
|
|
);
|
|
if (actionableIndex !== -1) {
|
|
return actionableIndex;
|
|
}
|
|
const firstIncomplete = stageStates.findIndex(
|
|
(stage) => !stage.isCompleted
|
|
);
|
|
if (firstIncomplete !== -1) {
|
|
return firstIncomplete;
|
|
}
|
|
const firstUnlocked = stageStates.findIndex((stage) => !stage.isLocked);
|
|
if (firstUnlocked !== -1) {
|
|
return firstUnlocked;
|
|
}
|
|
return stageStates.length - 1;
|
|
}, [stageStates]);
|
|
|
|
const [activeStageIndex, setActiveStageIndex] = useState(preferredStageIndex);
|
|
const [menuVisible, setMenuVisible] = useState(false);
|
|
const [menuAnchor, setMenuAnchor] = useState(null);
|
|
const [menuProject, setMenuProject] = useState(null);
|
|
const menuAnchorRef = useRef(null);
|
|
const testLoaderTimeoutRef = useRef(null);
|
|
const [isIntroVideoVisible, setIsIntroVideoVisible] = useState(false);
|
|
const [hasLocalAdventureFlag, setHasLocalAdventureFlag] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setActiveStageIndex((prev) => {
|
|
const fallbackIndex =
|
|
preferredStageIndex >= 0 && preferredStageIndex < stageStates.length
|
|
? preferredStageIndex
|
|
: 0;
|
|
|
|
if (prev == null || prev >= stageStates.length) {
|
|
return fallbackIndex;
|
|
}
|
|
const current = stageStates[prev];
|
|
if (!current) {
|
|
return fallbackIndex;
|
|
}
|
|
if (
|
|
(current.isLocked || current.isCompleted) &&
|
|
fallbackIndex !== prev &&
|
|
stageStates[fallbackIndex]
|
|
) {
|
|
return fallbackIndex;
|
|
}
|
|
return prev;
|
|
});
|
|
}, [stageStates, preferredStageIndex]);
|
|
|
|
const handleSelectProject = useCallback(
|
|
(project) => {
|
|
if (!project?.id) return;
|
|
selectProject(project.id);
|
|
setActiveStageIndex(findFirstUnlockedStageIndex(project));
|
|
},
|
|
[selectProject]
|
|
);
|
|
|
|
const handleCloseMenu = useCallback(() => {
|
|
setMenuVisible(false);
|
|
setMenuAnchor(null);
|
|
setMenuProject(null);
|
|
menuAnchorRef.current = null;
|
|
}, []);
|
|
|
|
const handleDeleteProject = useCallback(async () => {
|
|
if (!menuProject?.id) return;
|
|
try {
|
|
await projectsRef.doc(menuProject.id).delete();
|
|
setTooltip?.({ type: "success", text: "Projet supprimé" });
|
|
} catch (error) {
|
|
setTooltip?.({
|
|
type: "error",
|
|
text: error?.message || "Suppression impossible",
|
|
});
|
|
} finally {
|
|
handleCloseMenu();
|
|
}
|
|
}, [handleCloseMenu, menuProject?.id, setTooltip]);
|
|
|
|
const handleModifyProject = useCallback((project, anchor) => {
|
|
if (!project?.id) return;
|
|
|
|
const prevAnchor = menuAnchorRef.current;
|
|
const normalizedAnchor =
|
|
typeof anchor === "number"
|
|
? { top: anchor }
|
|
: anchor && typeof anchor === "object"
|
|
? anchor
|
|
: {};
|
|
|
|
setMenuProject(project);
|
|
setMenuAnchor(normalizedAnchor);
|
|
setMenuVisible((prev) => {
|
|
if (!prev) return true;
|
|
const prevTop =
|
|
typeof prevAnchor?.top === "number" ? prevAnchor.top : null;
|
|
const nextTop =
|
|
typeof normalizedAnchor?.top === "number" ? normalizedAnchor.top : null;
|
|
return prevTop !== nextTop;
|
|
});
|
|
menuAnchorRef.current = normalizedAnchor;
|
|
}, []);
|
|
|
|
const moreMenuItems = useMemo(() => {
|
|
if (!menuProject?.id) return [];
|
|
return [
|
|
{
|
|
label: "Supprimer",
|
|
onPress: () =>
|
|
Alert(
|
|
"Confirmer la suppression",
|
|
"Cette action supprimera définitivement ce projet.",
|
|
[
|
|
{ text: "Annuler", style: "cancel" },
|
|
{
|
|
text: "Supprimer",
|
|
style: "destructive",
|
|
onPress: () => {
|
|
handleDeleteProject();
|
|
},
|
|
},
|
|
],
|
|
{ cancelable: true }
|
|
),
|
|
},
|
|
];
|
|
}, [handleDeleteProject, menuProject?.id]);
|
|
|
|
useEffect(() => {
|
|
if (!route?.params?.showLyricsCongrats) {
|
|
return;
|
|
}
|
|
const beatmakerStage = currentProject
|
|
? getStageAction("beatmaker", currentProject)
|
|
: null;
|
|
Alert(
|
|
"Céline",
|
|
"Bravo ! Vous venez de terminer de créer les paroles de votre musique ! Maintenant vous pouvez passer à la prochaine étape : le Studio avec Malik. Dans cette étape vous allez pouvoir donner vie à votre chanson !",
|
|
[
|
|
{
|
|
text: "Retour à l'accueil",
|
|
style: "cancel",
|
|
},
|
|
{
|
|
text: "Continuer avec Malik",
|
|
onPress: () => {
|
|
if (!currentProject?.id) {
|
|
return;
|
|
}
|
|
if (selectedProject?.id !== currentProject.id) {
|
|
selectProject(currentProject.id);
|
|
}
|
|
const targetRoute = beatmakerStage?.route || Routes.Compose;
|
|
navigate(targetRoute, beatmakerStage?.params);
|
|
},
|
|
},
|
|
]
|
|
);
|
|
navigation?.setParams?.({ showLyricsCongrats: false });
|
|
}, [
|
|
currentProject,
|
|
navigation,
|
|
selectProject,
|
|
selectedProject?.id,
|
|
route?.params?.showLyricsCongrats,
|
|
]);
|
|
|
|
const activeStage =
|
|
stageStates[activeStageIndex] || stageStates[preferredStageIndex];
|
|
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 hasActiveProject = !!currentProject;
|
|
|
|
const ensureProjectSelected = useCallback(() => {
|
|
if (!currentProject?.id) {
|
|
return;
|
|
}
|
|
if (selectedProject?.id !== currentProject.id) {
|
|
selectProject(currentProject.id);
|
|
}
|
|
}, [currentProject?.id, selectProject, selectedProject?.id]);
|
|
|
|
const handleStageAction = useCallback(() => {
|
|
if (!hasActiveProject || stageLocked || !stageRoute) {
|
|
return;
|
|
}
|
|
ensureProjectSelected();
|
|
navigate(stageRoute, stageParams);
|
|
}, [
|
|
ensureProjectSelected,
|
|
hasActiveProject,
|
|
stageLocked,
|
|
stageParams,
|
|
stageRoute,
|
|
]);
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
if (testLoaderTimeoutRef.current) {
|
|
clearTimeout(testLoaderTimeoutRef.current);
|
|
testLoaderTimeoutRef.current = null;
|
|
}
|
|
};
|
|
}, []);
|
|
|
|
const songwriterAction = useMemo(
|
|
() => getStageAction("songwriter", null),
|
|
[]
|
|
);
|
|
|
|
const handleStartNew = useCallback(async () => {
|
|
const targetRoute = songwriterAction?.route || Routes.WritingLyrics;
|
|
const targetParams = songwriterAction?.params;
|
|
|
|
try {
|
|
const emptyProject =
|
|
projects.find((project) => isProjectEmpty(project)) || null;
|
|
|
|
if (emptyProject?.id) {
|
|
selectProject(emptyProject.id);
|
|
setActiveStageIndex(findFirstUnlockedStageIndex(emptyProject));
|
|
navigate(targetRoute, targetParams);
|
|
return;
|
|
}
|
|
|
|
const newProjectId = await createNewProject({ hasLyrics: false });
|
|
if (!newProjectId) {
|
|
return;
|
|
}
|
|
setActiveStageIndex(0);
|
|
navigate(targetRoute, targetParams);
|
|
} catch (error) {
|
|
console.warn("Home: unable to start new project", error);
|
|
setTooltip?.({
|
|
type: "error",
|
|
text: "Impossible de démarrer un nouveau projet",
|
|
});
|
|
}
|
|
}, [
|
|
createNewProject,
|
|
projects,
|
|
selectProject,
|
|
setActiveStageIndex,
|
|
setTooltip,
|
|
songwriterAction,
|
|
]);
|
|
|
|
const continueDisabled = !hasActiveProject || stageLocked || !stageRoute;
|
|
const startDisabled = !songwriterAction?.route;
|
|
|
|
useEffect(() => {
|
|
if (currentUserData?.adventureStarted) {
|
|
setHasLocalAdventureFlag(true);
|
|
}
|
|
}, [currentUserData?.adventureStarted]);
|
|
|
|
const persistAdventureStarted = useCallback(async () => {
|
|
if (!currentUID) {
|
|
return;
|
|
}
|
|
await usersRef
|
|
.doc(currentUID)
|
|
.set({ adventureStarted: true }, { merge: true });
|
|
}, [currentUID]);
|
|
|
|
const markAdventureStarted = useCallback(() => {
|
|
if (hasLocalAdventureFlag) {
|
|
return;
|
|
}
|
|
setHasLocalAdventureFlag(true);
|
|
persistAdventureStarted().catch((error) => {
|
|
console.warn("Home: adventureStarted update failed", error);
|
|
setHasLocalAdventureFlag(false);
|
|
setTooltip?.({
|
|
type: "error",
|
|
text: "Impossible de mettre à jour votre profil",
|
|
});
|
|
});
|
|
}, [hasLocalAdventureFlag, persistAdventureStarted, setTooltip]);
|
|
|
|
const handleStartVisit = useCallback(() => {
|
|
setIsIntroVideoVisible(true);
|
|
}, []);
|
|
|
|
const handleIntroVideoClose = useCallback(() => {
|
|
setIsIntroVideoVisible(false);
|
|
markAdventureStarted();
|
|
}, [markAdventureStarted]);
|
|
|
|
const adventureStarted =
|
|
hasLocalAdventureFlag || !!currentUserData?.adventureStarted;
|
|
|
|
const { data: video } = useDataFromRef({
|
|
ref: videosRef.doc("fr"),
|
|
simpleRef: true,
|
|
});
|
|
const videoUrl = isWeb ? video?.landingWeb : video?.landing;
|
|
const homeBackgroundImage = isWeb
|
|
? background.homeBGWeb
|
|
: background.homeBG;
|
|
const landingBackgroundImage = homeBackgroundImage;
|
|
|
|
return adventureStarted ? (
|
|
<Page
|
|
shareBtn
|
|
headerType="NONE"
|
|
containerStyle={{ width: "100%", maxWidth: "100%", paddingHorizontal: 0 }}
|
|
backgroundImg={homeBackgroundImage}
|
|
>
|
|
<View style={[styles.root, isWeb ? styles.rootWeb : styles.rootNative]}>
|
|
<View
|
|
style={{
|
|
zIndex: 10,
|
|
position: "absolute",
|
|
top: 10,
|
|
width: 400,
|
|
alignSelf: "center",
|
|
flexDirection: isWeb ? "column" : "row",
|
|
alignItems: "center",
|
|
paddingHorizontal: isWeb ? 0 : 30,
|
|
gap: 10,
|
|
}}
|
|
>
|
|
<ProjectDropDown
|
|
style={[isWeb ? { width: "100%" } : { flex: 1 }]}
|
|
projects={projects}
|
|
selectedProject={currentProject}
|
|
allowEmptySelection
|
|
onSelectProject={handleSelectProject}
|
|
onModifyProject={handleModifyProject}
|
|
onCreateProject={handleStartNew}
|
|
formatDate={formatDate}
|
|
/>
|
|
{!isWeb && <ShareBtn />}
|
|
</View>
|
|
<MoreMenu
|
|
visible={menuVisible}
|
|
top={menuAnchor?.top ?? 0}
|
|
position={menuAnchor}
|
|
onClose={handleCloseMenu}
|
|
inPlaylist={false}
|
|
projectId={menuProject?.id || null}
|
|
extraItems={moreMenuItems}
|
|
/>
|
|
<View style={[styles.carouselSection, styles.carouselSectionElevated]}>
|
|
<FeatureCarousel
|
|
selectedProject={currentProject}
|
|
stageStates={stageStates}
|
|
activeIndex={activeStageIndex}
|
|
onActiveIndexChange={setActiveStageIndex}
|
|
backgroundImage={homeBackgroundImage}
|
|
isFocused={isFocused}
|
|
/>
|
|
</View>
|
|
<View
|
|
style={{
|
|
position: "absolute",
|
|
bottom: isWeb ? 180 : 140,
|
|
flexDirection: isWeb ? "row" : "column",
|
|
gap: 5,
|
|
alignSelf: "center",
|
|
}}
|
|
>
|
|
<GradientButton
|
|
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
|
|
title={"Commencer à créer"}
|
|
onPress={handleStartNew}
|
|
disabled={startDisabled}
|
|
/>
|
|
<BorderGradientButton
|
|
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
|
|
title={"Continuer la création"}
|
|
onPress={handleStageAction}
|
|
disabled={continueDisabled}
|
|
/>
|
|
|
|
{/* <BorderGradientButton
|
|
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
|
|
title={"Test Alert"}
|
|
onPress={() =>
|
|
Alert("Test", "Ceci est un test d'alert", [
|
|
{ text: "Annuler", style: "cancel" },
|
|
{ text: "OK", onPress: () => console.log("OK pressé") },
|
|
])
|
|
}
|
|
/> */}
|
|
{/* <BorderGradientButton
|
|
containerStyle={{ width: Platform.OS === "web" ? 250 : "100%" }}
|
|
title={"Continuer la création"}
|
|
onPress={() => navigate(Routes.SongReady)}
|
|
disabled={continueDisabled}
|
|
/> */}
|
|
</View>
|
|
</View>
|
|
</Page>
|
|
) : (
|
|
<>
|
|
<Page
|
|
shareBtn
|
|
title="Landing Page"
|
|
backgroundImg={landingBackgroundImage}
|
|
>
|
|
<View
|
|
style={{
|
|
flex: 1,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<GradientButton
|
|
url={videos.landing}
|
|
title="Commencer l'aventure MusicLand"
|
|
onPress={handleStartVisit}
|
|
/>
|
|
</View>
|
|
</Page>
|
|
<FullscreenIntroVideo
|
|
url={videoUrl}
|
|
visible={isIntroVideoVisible}
|
|
onClose={handleIntroVideoClose}
|
|
/>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default Home;
|
|
|
|
const styles = StyleSheet.create({
|
|
root: {
|
|
flex: 1,
|
|
},
|
|
rootWeb: {
|
|
backgroundColor: "transparent",
|
|
},
|
|
rootNative: {
|
|
backgroundColor: "rgba(66, 91, 135, 0.3)",
|
|
},
|
|
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,
|
|
},
|
|
carouselSectionElevated: {
|
|
marginTop: -100,
|
|
paddingTop: 80,
|
|
},
|
|
});
|