Files
musicland/src/screens/Home/Home.js
T
2026-03-11 11:54:10 +01:00

831 lines
23 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 dinterpreter 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 tinté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 <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 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 = (
<View style={stageCardContainerStyle}>
{stageCards.map((card) => (
<StageCard
key={card.key}
step={card.step}
description={card.description}
image={card.image}
imagePosition={card.imagePosition}
textAlign={card.textAlign}
isLocked={card.isLocked}
lockSide={card.lockSide}
onPress={() => handleStagePress(card.key, card.isLocked)}
containerStyle={stageCardItemStyle}
variant={isWeb ? 'web' : 'mobile'}
/>
))}
</View>
)
const clubCard = (
<ClubCard onPress={handleClubPress} containerStyle={stageCardItemStyle} variant="stage" />
)
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 ? (
<View style={styles.mobileInfoRow}>
<Pressable
onPress={handleOpenCoinModal}
accessibilityRole="button"
style={styles.mobileCoinButton}
>
<CreditAmount
value={coinBalance}
style={styles.mobileCoinAmount}
textStyle={styles.mobileCoinText}
iconSize={20}
iconPosition="left"
/>
</Pressable>
<ShareBtn style={styles.mobileShareButton} label="Partager" />
</View>
) : null
const returnHomeButton = (
<Pressable onPress={handleReturnToLanding}>
<BlurView
tint="dark"
intensity={30}
style={{
paddingHorizontal: 10,
paddingVertical: 8,
borderRadius: 12,
overflow: 'hidden',
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Text
style={{
fontSize: 14,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
marginRight: 5,
}}
>
{"Retour à l'accueil"}
</Text>
</BlurView>
</Pressable>
)
const topBar = (
<View style={styles.topBar}>
{/* {returnHomeButton} */}
{mobileInfoRow}
{!isWeb ? returnHomeButton : null}
<ProjectDropDown
ref={projectDropdownRef}
style={styles.projectDropDown}
projects={projects}
selectedProject={currentProject}
allowEmptySelection
onSelectProject={handleSelectProject}
onModifyProject={handleModifyProject}
onPlayProject={handlePlayProject}
onCreateProject={handleStartNew}
formatDate={formatDate}
/>
</View>
)
const journeyContent = (
<>
<MoreMenu
visible={menuVisible}
top={menuAnchor?.top ?? 0}
position={menuAnchor}
onClose={handleCloseMenu}
inPlaylist={false}
projectId={menuProject?.id || null}
extraItems={moreMenuItems}
/>
<View style={styles.subtitleWrapper}>
{isWeb ? (
<LinearGradient
colors={['#F94697', '#7023F7']}
start={{ x: 0, y: 0.5 }}
end={{ x: 1, y: 0.5 }}
style={[styles.subtitleGradient, styles.subtitleGradientWeb]}
>
<View style={[styles.subtitleInner, styles.subtitleInnerWeb]}>
<Text style={styles.subtitle}>5 espaces à découvrir</Text>
</View>
</LinearGradient>
) : (
<BorderGradient
style={[styles.subtitleGradient, styles.subtitleBorder]}
gradientProps={{
colors: ['#F94697', '#7023F7'],
start: { x: 0, y: 0.5 },
end: { x: 1, y: 0.5 },
locations: [0, 1],
}}
>
<View style={styles.subtitleInner}>
<Text style={styles.subtitle}>5 espaces à découvrir</Text>
</View>
</BorderGradient>
)}
</View>
{stageCardsList}
{isWeb ? (
<View style={styles.webClubWrapper}>{clubCard}</View>
) : (
<View style={styles.mobileClubWrapper}>{clubCard}</View>
)}
</>
)
return (
<>
{/* {adventureStarted ? ( */}
<View style={styles.root}>
<Page
shareBtn
headerType="NONE"
scrollEnabled={false}
containerStyle={styles.page}
contentContainerStyle={styles.pageContent}
maxWidth={1600}
width="100%"
backgroundColor="#303438"
showReturnHome={isWeb}
onReturnHome={handleReturnToLanding}
>
<View style={styles.inner}>
<ExpoImage source={homeBackgroundImage} contentFit="cover" style={styles.centerImage} />
{isWeb ? (
<>
{/* <View style={styles.webReturnButtonWrapper}>
{returnHomeButton}
</View> */}
<View style={styles.contentOverlay}>
{topBar}
{journeyContent}
</View>
</>
) : (
<View style={[styles.contentOverlay, styles.mobileContent]}>
{topBar}
<ScrollView
style={styles.mobileScrollView}
contentContainerStyle={styles.mobileScrollContent}
showsVerticalScrollIndicator={false}
>
{journeyContent}
</ScrollView>
</View>
)}
</View>
</Page>
</View>
</>
)
}
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,
},
})