import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Image as ExpoImage } from "expo-image";
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
import useMinuit from "react-native-minuit/src/hooks/useMinuit.js";
import { background, cardsImg, icons } from "../../assets";
import Alert from "../../components/Alert";
import BorderGradient from "../../components/BorderGradient/BorderGradient";
import { LinearGradient } from "../../components/LinearGradient/LinearGradient";
import FullscreenIntroVideo from "../../components/FullscreenIntroVideo";
import GradientButton from "../../components/GradientButton";
import MoreMenu from "../../components/MoreMenu";
import ProjectDropDown from "../../components/ProjectDropDown/ProjectDropDown";
import CoinPackModal from "../../components/modal/CoinPackModal";
import { projectsRef, usersRef } from "../../config/firebase";
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 CreditAmount from "../../components/CreditAmount";
import ShareBtn from "../../components/ShareBtn/ShareBtn";
import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import {
getCreationStageStates,
getStageAction,
} from "../../utils/projectStages";
import { openCoinPackModal } from "../../utils/coinPackModal";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import StageCard from "./components/StageCard";
import ClubCard from "./components/ClubCard";
const isProjectEmpty = (project) => {
if (!project) {
return false;
}
return !(
typeof project?.title === "string" && project.title.trim().length > 0
);
};
const HOME_BACKGROUND_WIDTH = isWeb ? 1280 : 520;
const HOME_BACKGROUND_HEIGHT = isWeb ? 760 : 360;
const HOME_BACKGROUND_STYLE_WIDTH = HOME_BACKGROUND_WIDTH + 120;
const HOME_BACKGROUND_STYLE_HEIGHT = HOME_BACKGROUND_HEIGHT + 80;
const STAGE_CARD_CONTENT = [
{
key: "songwriter",
step: "ÉTAPE 1",
description:
"Ensemble, nous allons écrire ta chanson.\nMéthodiquement je vais te guider pour construire une oeuvre unique et authentique.",
image: cardsImg.writing,
imagePosition: "left",
textAlign: "left",
lockSide: "right",
},
{
key: "beatmaker",
step: "ÉTAPE 2",
description:
"Je suis Malik, responsable du studio de MusicLand.\nJe vais mettre en musique tes paroles en fonction de tes goûts musicaux, ça va être top !",
image: cardsImg.studio,
imagePosition: "right",
textAlign: "right",
lockSide: "left",
},
{
key: "director",
step: "ÉTAPE 3",
description:
"Tu vas faire une expérience extraordinaire, tu vas te filmer en train d’interpreter ta chanson en Play Back et je vais te guider pour te faciliter la tâche!",
image: cardsImg.video,
imagePosition: "left",
textAlign: "left",
lockSide: "right",
},
{
key: "publisher",
step: "ÉTAPE 4",
description:
"Je suis Mr Benhaï, Producteur de MusicLand, et je vais te faire une proposition qui pourrait t’intéresser. On se retrouve à la sortie du studio !",
image: cardsImg.production,
imagePosition: "right",
textAlign: "right",
lockSide: "left",
},
];
const ACTIVE_SUBSCRIPTION_STATUSES = new Set([
"trialing",
"active",
"past_due",
"unpaid",
]);
const Home = ({ navigation, route }) => {
const {
userProjects = [],
selectProject,
selectedProject,
selectedProjectId,
currentUserData,
currentUID,
createNewProject,
videos,
hasActiveSubscription,
} = useUser();
const { setTooltip } = useMinuit();
const [videoUrl, setVideoUrl] = useState(null);
const navigateToMusicDetails = useNavigateToMusicDetails();
if (!currentUID) {
return ;
}
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 stageStatesByKey = useMemo(() => {
if (!Array.isArray(stageStates)) {
return {};
}
return stageStates.reduce((acc, stage) => {
if (stage?.key) {
acc[stage.key] = stage;
}
return acc;
}, {});
}, [stageStates]);
const [menuVisible, setMenuVisible] = useState(false);
const [menuAnchor, setMenuAnchor] = useState(null);
const [menuProject, setMenuProject] = useState(null);
const menuAnchorRef = useRef(null);
const projectDropdownRef = useRef(null);
const [hasLocalAdventureFlag, setHasLocalAdventureFlag] = useState(false);
const handleSelectProject = useCallback(
(project) => {
if (!project?.id) return;
selectProject(project.id);
},
[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 [];
const items = [];
const hasSongUrl =
typeof menuProject?.songUrl === "string" &&
menuProject.songUrl.length > 0;
if (hasSongUrl) {
items.push({
label: "Jouer la musique",
onPress: () => {
projectDropdownRef.current?.close?.();
navigateToMusicDetails({
projectId: menuProject.id,
songUrl: menuProject.songUrl,
project: menuProject,
});
},
});
}
items.push({
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 },
),
});
return items;
}, [handleDeleteProject, menuProject, navigateToMusicDetails]);
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 pour donner vie à votre chanson !",
[
{
text: "Retour à l'accueil",
style: "cancel",
},
{
text: "Continuer vers le Studio",
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 hasActiveProject = !!currentProject;
const ensureProjectSelected = useCallback(() => {
if (!currentProject?.id) {
return;
}
if (selectedProject?.id !== currentProject.id) {
selectProject(currentProject.id);
}
}, [currentProject?.id, selectProject, selectedProject?.id]);
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);
navigate(targetRoute, targetParams);
return;
}
const newProjectId = await createNewProject({ hasLyrics: false });
if (!newProjectId) {
return;
}
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, setTooltip, songwriterAction]);
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(() => {
setVideoUrl(isWeb ? videos?.landingWeb : videos?.landing);
}, []);
const handleIntroVideoClose = useCallback(() => {
setVideoUrl(null);
markAdventureStarted();
}, [markAdventureStarted]);
const adventureStarted =
hasLocalAdventureFlag || !!currentUserData?.adventureStarted;
const homeBackgroundImage = background.bgTrans;
const landingBackgroundImage = homeBackgroundImage;
const stageCards = useMemo(
() =>
STAGE_CARD_CONTENT.map((card) => ({
...card,
isLocked: stageStatesByKey[card.key]?.isLocked ?? true,
})),
[stageStatesByKey],
);
const [isCoinModalVisible, setCoinModalVisible] = useState(false);
const handleStagePress = useCallback(
(stageKey, isLocked) => {
if (isLocked) {
return;
}
if (!hasActiveProject || !currentProject) {
if (stageKey === "songwriter") {
handleStartNew();
}
return;
}
ensureProjectSelected();
const action = getStageAction(stageKey, currentProject);
if (!action?.route) {
return;
}
navigate(action.route, action.params);
},
[currentProject, ensureProjectSelected, hasActiveProject, handleStartNew],
);
const handleClubPress = useCallback(() => {
navigate(Routes.Payments);
}, []);
const handleOpenCoinModal = useCallback(() => {
if (isWeb) {
openCoinPackModal();
return;
}
setCoinModalVisible(true);
}, []);
const handleCloseCoinModal = useCallback(() => {
setCoinModalVisible(false);
}, []);
const handleNavigateToLanding = useCallback(() => {
navigate(Routes.LandingPage);
}, []);
const stageCardContainerStyle = isWeb ? styles.cardsGrid : styles.cardsStack;
const stageCardItemStyle = isWeb
? styles.webStageCard
: styles.mobileStageCard;
const stageCardsList = (
{stageCards.map((card) => (
handleStagePress(card.key, card.isLocked)}
containerStyle={stageCardItemStyle}
variant={isWeb ? "web" : "mobile"}
/>
))}
);
const coinBalance = useMemo(() => {
const value = currentUserData?.coins;
if (typeof value === "number" && Number.isFinite(value)) {
return value;
}
if (typeof value === "string") {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return 0;
}, [currentUserData?.coins]);
const mobileInfoRow = !isWeb ? (
) : null;
const topBar = (
Retour à l'accueil
{mobileInfoRow}
);
const journeyContent = (
<>
{isWeb ? (
5 espaces à découvrir
) : (
5 espaces à découvrir
)}
{stageCardsList}
>
);
return (
<>
{adventureStarted ? (
{isWeb ? (
{topBar}
{journeyContent}
) : (
{topBar}
{journeyContent}
)}
) : (
<>
>
)}
{!isWeb ? (
) : null}
>
);
};
export default Home;
const styles = StyleSheet.create({
root: {
flex: 1,
backgroundColor: "#303438",
},
page: {
backgroundColor: "transparent",
padding: 0,
paddingTop: 0,
paddingBottom: 0,
width: "100%",
},
pageContent: {
flexGrow: 1,
},
inner: {
flex: 1,
width: "100%",
alignSelf: "stretch",
alignItems: "stretch",
justifyContent: "flex-start",
position: "relative",
},
centerImage: {
width: HOME_BACKGROUND_STYLE_WIDTH,
height: HOME_BACKGROUND_STYLE_HEIGHT,
borderRadius: 22,
overflow: "hidden",
position: "absolute",
top: isWeb ? "45%" : "50%",
left: "50%",
transform: [
{ translateX: -HOME_BACKGROUND_STYLE_WIDTH / 2 },
{ translateY: -HOME_BACKGROUND_STYLE_HEIGHT / 2 },
],
pointerEvents: "none",
zIndex: 0,
},
contentOverlay: {
position: "relative",
width: "100%",
flexGrow: 1,
},
topBar: {
width: "100%",
flexDirection: isWeb ? "row" : "column",
flexWrap: isWeb ? "wrap" : "nowrap",
alignItems: isWeb ? "center" : "stretch",
justifyContent: isWeb ? "center" : "flex-start",
gap: isWeb ? 0 : 12,
marginTop: isWeb ? 12 : 0,
marginBottom: isWeb ? 4 : 16,
paddingHorizontal: isWeb ? 0 : gutters,
zIndex: 10,
position: "relative",
},
returnLandingButton: {
paddingHorizontal: 14,
paddingVertical: 10,
borderRadius: 18,
backgroundColor: "rgba(12, 14, 18, 0.72)",
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.1)",
alignSelf: "flex-start",
},
returnLandingButtonWeb: {
position: "absolute",
left: 0,
top: 0,
transform: [{ translateY: -2 }],
},
returnLandingButtonText: {
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
},
topBarContent: {
flex: 1,
width: "100%",
flexDirection: isWeb ? "row" : "column",
alignItems: isWeb ? "center" : "stretch",
justifyContent: isWeb ? "center" : "flex-start",
gap: 12,
},
projectDropDown: {
width: isWeb ? 420 : "100%",
maxWidth: 420,
flexGrow: 1,
},
mobileInfoRow: {
width: "100%",
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: 12,
},
mobileCoinButton: {
flexDirection: "row",
alignItems: "center",
gap: 8,
paddingHorizontal: 14,
paddingVertical: 8,
borderRadius: 20,
backgroundColor: "rgba(12, 14, 18, 0.72)",
borderWidth: 1,
borderColor: "rgba(255, 255, 255, 0.1)",
flexShrink: 1,
},
mobileCoinAmount: {
flexDirection: "row",
alignItems: "center",
gap: 6,
},
mobileCoinText: {
fontSize: 18,
color: Palette.white,
fontFamily: FONT_FAMILY.InterSemiBold,
},
mobileShareButton: {
flexShrink: 0,
},
mobileContent: {
flex: 1,
width: "100%",
},
mobileScrollView: {
flex: 1,
width: "100%",
},
mobileScrollContent: {
paddingHorizontal: gutters,
paddingTop: 0,
paddingBottom: gutters * 6,
},
subtitleWrapper: {
width: "100%",
marginBottom: isWeb ? 12 : 16,
marginTop: isWeb ? 8 : 15,
alignItems: "center",
},
subtitleGradient: {
width: "100%",
maxWidth: 420,
borderRadius: 999,
alignSelf: "center",
},
subtitleBorder: {
borderWidth: 1,
},
subtitleGradientWeb: {
padding: 2,
},
subtitleInner: {
width: "100%",
paddingHorizontal: 24,
paddingVertical: 10,
alignItems: "center",
justifyContent: "center",
borderRadius: 999,
backgroundColor: Palette.glass,
},
subtitleInnerWeb: {
paddingVertical: 10,
backgroundColor: Palette.lightPurple,
},
subtitle: {
fontFamily: FONT_FAMILY.InterMedium,
fontSize: 18,
color: Palette.white,
textAlign: "center",
textTransform: "uppercase",
letterSpacing: 1,
},
cardsGrid: {
width: "100%",
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between",
rowGap: isWeb ? 16 : 20,
},
cardsStack: {
width: "100%",
flexDirection: "column",
alignItems: "stretch",
},
webStageCard: {
width: "48%",
},
mobileStageCard: {
width: "100%",
marginBottom: 20,
},
});