feat: montly songs
This commit is contained in:
@@ -3,7 +3,6 @@ import {
|
|||||||
FlatList,
|
FlatList,
|
||||||
Image,
|
Image,
|
||||||
Platform,
|
Platform,
|
||||||
Pressable,
|
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
@@ -20,17 +19,13 @@ import useDataFromRef from '../../hooks/useDataFromRef'
|
|||||||
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
|
import useNavigateToMusicDetails from '../../hooks/useNavigateToMusicDetails'
|
||||||
import useSearch from '../../hooks/useSearch'
|
import useSearch from '../../hooks/useSearch'
|
||||||
import Page from '../../layouts/Page'
|
import Page from '../../layouts/Page'
|
||||||
import { Routes } from '../../navigation'
|
|
||||||
import { navigate } from '../../navigation/NavigationService'
|
|
||||||
import { Palette, Style } from '../../styles'
|
import { Palette, Style } from '../../styles'
|
||||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||||
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
import CreateLyricsHeader from '../Writing/components/CreateLyricsHeader'
|
||||||
import MobileCoinBadge from '../../components/MobileCoinBadge'
|
import MobileCoinBadge from '../../components/MobileCoinBadge'
|
||||||
import ResearchHeader from '../Library/components/ResearchHeader'
|
import ResearchHeader from '../Library/components/ResearchHeader'
|
||||||
import SearchResultsList from '../Library/components/SearchResultsList'
|
import SearchResultsList from '../Library/components/SearchResultsList'
|
||||||
import PlaybacksCard from './components/PlaybacksCard'
|
|
||||||
import SongCard from './components/SongCard'
|
import SongCard from './components/SongCard'
|
||||||
import BorderGradient from '../../components/BorderGradient/BorderGradient.web'
|
|
||||||
|
|
||||||
const allowedPremiumLevels = new Set(['starter', 'pro', 'premium'])
|
const allowedPremiumLevels = new Set(['starter', 'pro', 'premium'])
|
||||||
|
|
||||||
@@ -53,34 +48,37 @@ const chunkArray = (items = [], size = 10) => {
|
|||||||
const HitParade = () => {
|
const HitParade = () => {
|
||||||
const [selectedCategory, setSelectedCategory] = useState('Chansons')
|
const [selectedCategory, setSelectedCategory] = useState('Chansons')
|
||||||
const navigateToMusicDetails = useNavigateToMusicDetails()
|
const navigateToMusicDetails = useNavigateToMusicDetails()
|
||||||
const { data: topSongs } = useDataFromRef({
|
const { data: allSongs } = useDataFromRef({
|
||||||
ref: projectsRef.where('views', '>', 0).orderBy('views', 'desc').limit(20),
|
ref: projectsRef.orderBy('updatedAt', 'desc'),
|
||||||
|
format: (docs) => docs.filter((item) => item?.hasPlayback !== true),
|
||||||
simpleRef: false,
|
simpleRef: false,
|
||||||
listener: true,
|
listener: true,
|
||||||
condition: true,
|
condition: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { data: topPlaybacks } = useDataFromRef({
|
const previousMonthKey = useMemo(() => {
|
||||||
ref: projectsRef.where('views', '>', 0).orderBy('views', 'desc').limit(60),
|
const now = new Date()
|
||||||
format: (docs) => docs.filter((d) => d?.playbackUrl != null).slice(0, 20),
|
const target = new Date(now.getFullYear(), now.getMonth(), 1)
|
||||||
simpleRef: false,
|
target.setMonth(target.getMonth() - 1)
|
||||||
listener: true,
|
const month = String(target.getMonth() + 1).padStart(2, '0')
|
||||||
condition: true,
|
return `${target.getFullYear()}-${month}`
|
||||||
})
|
|
||||||
|
|
||||||
const songsList = useMemo(() => (Array.isArray(topSongs) ? topSongs : []), [topSongs])
|
|
||||||
|
|
||||||
const playbackList = useMemo(
|
|
||||||
() => (Array.isArray(topPlaybacks) ? topPlaybacks : []),
|
|
||||||
[topPlaybacks]
|
|
||||||
)
|
|
||||||
|
|
||||||
const resolvePlaybackThumbnail = useCallback((project) => {
|
|
||||||
const candidates = [project?.thumbnailUrl, project?.songThumbnailUrl, project?.coverUrl]
|
|
||||||
const uri = candidates.find((value) => typeof value === 'string' && value.trim().length > 0)
|
|
||||||
return uri || null
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const { data: monthlyTopDoc } = useDataFromRef({
|
||||||
|
ref: firebase.firestore().collection('monthlyTopSongs').doc(previousMonthKey),
|
||||||
|
initialState: null,
|
||||||
|
simpleRef: true,
|
||||||
|
listener: false,
|
||||||
|
condition: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const songsList = useMemo(() => (Array.isArray(allSongs) ? allSongs : []), [allSongs])
|
||||||
|
|
||||||
|
const topMonthList = useMemo(() => {
|
||||||
|
const items = monthlyTopDoc?.topProjects
|
||||||
|
return Array.isArray(items) ? items : []
|
||||||
|
}, [monthlyTopDoc])
|
||||||
|
|
||||||
// === RENDUS PAR PLATEFORME ===
|
// === RENDUS PAR PLATEFORME ===
|
||||||
const renderSongsMobile = () => (
|
const renderSongsMobile = () => (
|
||||||
<FlatList
|
<FlatList
|
||||||
@@ -88,11 +86,11 @@ const HitParade = () => {
|
|||||||
gap: 10,
|
gap: 10,
|
||||||
paddingBottom: responsiveHeight(20),
|
paddingBottom: responsiveHeight(20),
|
||||||
}}
|
}}
|
||||||
data={Array.isArray(topSongs) ? topSongs : []}
|
data={songsList}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item }) => (
|
||||||
<SongCard
|
<SongCard
|
||||||
rank={index + 1}
|
rank={null}
|
||||||
title={item?.title || 'Sans titre'}
|
title={item?.title || 'Sans titre'}
|
||||||
artist={item?.userName}
|
artist={item?.userName}
|
||||||
coverUrl={item?.coverUrl || null}
|
coverUrl={item?.coverUrl || null}
|
||||||
@@ -109,22 +107,27 @@ const HitParade = () => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderPlaybacksMobile = () => (
|
const renderTopMonthMobile = () => (
|
||||||
<FlatList
|
<FlatList
|
||||||
contentContainerStyle={{
|
contentContainerStyle={{
|
||||||
gap: 10,
|
gap: 10,
|
||||||
paddingBottom: responsiveHeight(20),
|
paddingBottom: responsiveHeight(20),
|
||||||
}}
|
}}
|
||||||
data={playbackList}
|
data={topMonthList}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.projectId}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item }) => (
|
||||||
<PlaybacksCard
|
<SongCard
|
||||||
rank={index + 1}
|
rank={item?.rank}
|
||||||
title={item?.title || 'Sans titre'}
|
title={item?.title || 'Sans titre'}
|
||||||
artist={item?.userName}
|
artist={item?.userName}
|
||||||
thumbnailUrl={resolvePlaybackThumbnail(item)}
|
coverUrl={item?.coverUrl || null}
|
||||||
subscriptionLevel={getCreatorLevel(item)}
|
subscriptionLevel={getCreatorLevel(item)}
|
||||||
onPress={() => navigate(Routes.Playbacks, { projectId: item.id })}
|
onPress={() =>
|
||||||
|
navigateToMusicDetails({
|
||||||
|
projectId: item?.projectId,
|
||||||
|
songUrl: item?.songUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -132,10 +135,10 @@ const HitParade = () => {
|
|||||||
|
|
||||||
const renderSongList = () => (
|
const renderSongList = () => (
|
||||||
<View style={{ gap: 10 }}>
|
<View style={{ gap: 10 }}>
|
||||||
{songsList.map((item, idx) => (
|
{songsList.map((item) => (
|
||||||
<SongCard
|
<SongCard
|
||||||
key={item.id}
|
key={item.id}
|
||||||
rank={idx + 1}
|
rank={null}
|
||||||
title={item?.title || 'Sans titre'}
|
title={item?.title || 'Sans titre'}
|
||||||
artist={item?.userName}
|
artist={item?.userName}
|
||||||
coverUrl={item?.coverUrl || null}
|
coverUrl={item?.coverUrl || null}
|
||||||
@@ -152,17 +155,22 @@ const HitParade = () => {
|
|||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|
||||||
const renderPlaybackColumns = () => (
|
const renderTopMonthColumns = () => (
|
||||||
<View style={{ gap: 10 }}>
|
<View style={{ gap: 10 }}>
|
||||||
{playbackList.map((item, idx) => (
|
{topMonthList.map((item) => (
|
||||||
<PlaybacksCard
|
<SongCard
|
||||||
key={item.id}
|
key={item.projectId}
|
||||||
rank={idx + 1}
|
rank={item?.rank}
|
||||||
title={item?.title || 'Sans titre'}
|
title={item?.title || 'Sans titre'}
|
||||||
artist={item?.userName}
|
artist={item?.userName}
|
||||||
thumbnailUrl={resolvePlaybackThumbnail(item)}
|
coverUrl={item?.coverUrl || null}
|
||||||
subscriptionLevel={getCreatorLevel(item)}
|
subscriptionLevel={getCreatorLevel(item)}
|
||||||
onPress={() => navigate(Routes.Playbacks, { projectId: item.id })}
|
onPress={() =>
|
||||||
|
navigateToMusicDetails({
|
||||||
|
projectId: item?.projectId,
|
||||||
|
songUrl: item?.songUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
@@ -204,9 +212,9 @@ const HitParade = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
songsList.forEach(collectUserId)
|
songsList.forEach(collectUserId)
|
||||||
playbackList.forEach(collectUserId)
|
topMonthList.forEach(collectUserId)
|
||||||
return Array.from(collected)
|
return Array.from(collected)
|
||||||
}, [playbackList, songsList])
|
}, [songsList, topMonthList])
|
||||||
|
|
||||||
const fetchCreatorsByIds = useCallback(
|
const fetchCreatorsByIds = useCallback(
|
||||||
async (userIds) => {
|
async (userIds) => {
|
||||||
@@ -332,7 +340,7 @@ const HitParade = () => {
|
|||||||
const shouldBlurContent = shouldShowResults
|
const shouldBlurContent = shouldShowResults
|
||||||
|
|
||||||
const handlePlayRandomSong = useCallback(() => {
|
const handlePlayRandomSong = useCallback(() => {
|
||||||
const arr = Array.isArray(topSongs) ? topSongs : []
|
const arr = songsList
|
||||||
|
|
||||||
if (!arr.length) {
|
if (!arr.length) {
|
||||||
console.warn('Aucune chanson disponible pour le moment.')
|
console.warn('Aucune chanson disponible pour le moment.')
|
||||||
@@ -346,17 +354,13 @@ const HitParade = () => {
|
|||||||
songUrl: randomSong?.songUrl,
|
songUrl: randomSong?.songUrl,
|
||||||
project: randomSong,
|
project: randomSong,
|
||||||
})
|
})
|
||||||
}, [navigateToMusicDetails, topSongs])
|
}, [navigateToMusicDetails, songsList])
|
||||||
|
|
||||||
const categoryLabel = selectedCategory === 'Playback' ? 'playbacks' : 'chansons'
|
|
||||||
|
|
||||||
const heroTitle = isWeb
|
const heroTitle = isWeb
|
||||||
? 'Coups de coeur des utilisateurs'
|
? 'Coups de coeur des utilisateurs'
|
||||||
: `Coups de coeur des utilisateurs catégorie ${selectedCategory}`
|
: `Coups de coeur des utilisateurs catégorie ${selectedCategory}`
|
||||||
|
|
||||||
const heroSubtitle = isWeb
|
const heroSubtitle = 'Les 3 musiques les plus likées du mois reçoivent des tokens.'
|
||||||
? 'Les 3 chansons et playbacks les plus likées du mois reçoivent des tokens.'
|
|
||||||
: `Les 3 ${categoryLabel} les plus likées du mois reçoivent des tokens.`
|
|
||||||
|
|
||||||
const renderHero = () => (
|
const renderHero = () => (
|
||||||
<View style={{ gap: 11, maxWidth: 800, width: '100%', alignSelf: 'center' }}>
|
<View style={{ gap: 11, maxWidth: 800, width: '100%', alignSelf: 'center' }}>
|
||||||
@@ -453,11 +457,11 @@ const HitParade = () => {
|
|||||||
fontFamily: FONT_FAMILY.HelveticaNeueBold,
|
fontFamily: FONT_FAMILY.HelveticaNeueBold,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Playbacks
|
Top du mois
|
||||||
</Text>
|
</Text>
|
||||||
{/* </BorderGradient> */}
|
{/* </BorderGradient> */}
|
||||||
</BlurView>
|
</BlurView>
|
||||||
{renderPlaybackColumns()}
|
{renderTopMonthColumns()}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
@@ -474,7 +478,7 @@ const HitParade = () => {
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{['Chansons', 'Playback'].map((item, index) => (
|
{['Chansons', 'Top du mois'].map((item, index) => (
|
||||||
<BorderGradientButton
|
<BorderGradientButton
|
||||||
key={index}
|
key={index}
|
||||||
title={item}
|
title={item}
|
||||||
@@ -490,7 +494,7 @@ const HitParade = () => {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{selectedCategory === 'Chansons' && renderSongsMobile()}
|
{selectedCategory === 'Chansons' && renderSongsMobile()}
|
||||||
{selectedCategory === 'Playback' && renderPlaybacksMobile()}
|
{selectedCategory === 'Top du mois' && renderTopMonthMobile()}
|
||||||
{/* "Clips" pourra reprendre la même logique si tu le réactives */}
|
{/* "Clips" pourra reprendre la même logique si tu le réactives */}
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export const buildMonthKey = (date = new Date()) => {
|
||||||
|
const target = date instanceof Date ? new Date(date) : new Date()
|
||||||
|
const year = target.getFullYear()
|
||||||
|
const month = String(target.getMonth() + 1).padStart(2, '0')
|
||||||
|
|
||||||
|
return `${year}-${month}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export default buildMonthKey
|
||||||
Reference in New Issue
Block a user