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 { 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 useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails' import usePlayer from '../../hooks/usePlayer' import StageCard from './components/StageCard' import ClubCard from './components/ClubCard' import { BlurView } from 'expo-blur' 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 { play } = usePlayer() 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 handlePlayProject = useCallback( (project) => { if (!project?.id || !project?.songUrl) return const cover = project.coverUrl || project.coverUri || project.cover play({ id: project.id, url: project.songUrl, title: project.title || 'Sans titre', artist: 'MusicLand', artwork: cover, coverUrl: typeof cover === 'string' ? cover : cover?.uri, metadata: { projectId: project.id, }, }) }, [play] ) const moreMenuItems = useMemo(() => { if (!menuProject?.id) return [] const items = [] const hasSongUrl = typeof menuProject?.songUrl === 'string' && menuProject.songUrl.length > 0 if (hasSongUrl) { items.push({ label: 'Télécharger', onPress: () => { projectDropdownRef.current?.close?.() navigate(Routes.SongDownload, { project: menuProject, skipAdventureGate: true, promptPurchaseConfirm: true, }) }, }) } 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, tu as créé les paroles de ta chanson! Prochaine étape, le studio! Malik t'attends pour mettre ça en musique! A bientôt! and...Feel The Good Vibe!", [ { 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 handleReturnToLanding = useCallback(() => { navigate(Routes.LandingPage) }, []) 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 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 } console.log("action route is ", action.route) navigate(action.route, action.params) }, [currentProject, ensureProjectSelected, hasActiveProject, handleStartNew] ) const handleClubPress = useCallback(() => { navigate(Routes.HitParade) }, []) const handleOpenCoinModal = useCallback(() => { navigate(Routes.Payments) }, []) 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 clubCard = ( ) 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 returnHomeButton = ( {"Retour à l'accueil"} ) const topBar = ( {/* {returnHomeButton} */} {mobileInfoRow} {!isWeb ? returnHomeButton : null} ) const journeyContent = ( <> {isWeb ? ( 5 espaces à découvrir ) : ( 5 espaces à découvrir )} {stageCardsList} {isWeb ? ( {clubCard} ) : ( {clubCard} )} ) return ( <> {/* {adventureStarted ? ( */} {isWeb ? ( <> {/* {returnHomeButton} */} {topBar} {journeyContent} ) : ( {topBar} {journeyContent} )} ) } 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, }, returnButton: { width: isWeb ? 220 : '100%', alignSelf: isWeb ? 'flex-start' : 'stretch', }, webReturnButtonWrapper: { alignSelf: 'flex-start', marginTop: gutters * 2.5, marginBottom: 8, }, 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', }, webClubWrapper: { width: '100%', alignItems: 'center', marginTop: 16, }, mobileClubWrapper: { width: '100%', marginTop: 20, }, webStageCard: { width: '48%', }, mobileStageCard: { width: '100%', marginBottom: 20, }, })