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,
},
})