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 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 = ( {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: 12, marginTop: isWeb ? 12 : 0, marginBottom: isWeb ? 4 : 16, paddingHorizontal: isWeb ? 0 : gutters, zIndex: 10, }, 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, }, });