feat: playbacks leaderboard
This commit is contained in:
@@ -1,18 +1,23 @@
|
||||
import { useIsFocused, useRoute } from '@react-navigation/native'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { View } from 'react-native'
|
||||
import { Pressable, StyleSheet, Text, View } from 'react-native'
|
||||
import Carousel from 'react-native-reanimated-carousel'
|
||||
import { responsiveHeight } from 'react-native-responsive-dimensions'
|
||||
import { projectsRef } from '../../config/firebase'
|
||||
import useDataFromRef from '../../hooks/useDataFromRef'
|
||||
import PlaybackItem from './components/PlaybackItem'
|
||||
import PlaybackCharts from './components/PlaybackCharts'
|
||||
import { Palette } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
|
||||
const Playbacks = () => {
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const userCache = useRef(new Map())
|
||||
const isFocused = useIsFocused()
|
||||
const route = useRoute()
|
||||
const focusProjectId = route?.params?.projectId || route?.params?.focusId || null
|
||||
const initialFocusProjectId = route?.params?.projectId || route?.params?.focusId || null
|
||||
const [focusProjectId, setFocusProjectId] = useState(initialFocusProjectId)
|
||||
const [viewMode, setViewMode] = useState(initialFocusProjectId ? 'feed' : 'charts')
|
||||
const { data: rawPlaybacks = [], loadMore } = useDataFromRef({
|
||||
ref: projectsRef.where('playbackUrl', '!=', null),
|
||||
simpleRef: false,
|
||||
@@ -75,6 +80,39 @@ const Playbacks = () => {
|
||||
}
|
||||
}, [focusIndex, focusProjectId, loadMore, playbacks])
|
||||
|
||||
useEffect(() => {
|
||||
const nextFocusId = route?.params?.projectId || route?.params?.focusId || null
|
||||
setFocusProjectId(nextFocusId)
|
||||
if (nextFocusId) {
|
||||
setViewMode('feed')
|
||||
}
|
||||
}, [route?.params?.focusId, route?.params?.projectId])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode !== 'feed') {
|
||||
return
|
||||
}
|
||||
setActiveIndex(0)
|
||||
}, [viewMode])
|
||||
|
||||
const showFeed = viewMode === 'feed'
|
||||
|
||||
const openFeedForPlayback = useCallback((project) => {
|
||||
const nextId =
|
||||
project && typeof project === 'object'
|
||||
? project?.id || project?.projectId || null
|
||||
: project || null
|
||||
if (!nextId) {
|
||||
return
|
||||
}
|
||||
setFocusProjectId(nextId)
|
||||
setViewMode('feed')
|
||||
}, [])
|
||||
|
||||
if (!showFeed) {
|
||||
return <PlaybackCharts onPlaybackPress={openFeedForPlayback} />
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
||||
<Carousel
|
||||
@@ -93,8 +131,34 @@ const Playbacks = () => {
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Pressable
|
||||
style={styles.backToCharts}
|
||||
hitSlop={12}
|
||||
onPress={() => setViewMode('charts')}
|
||||
accessibilityLabel="Revenir au classement des playbacks"
|
||||
>
|
||||
<Text style={styles.backToChartsText}>Classement</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
export default Playbacks
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
backToCharts: {
|
||||
position: 'absolute',
|
||||
top: 32,
|
||||
right: 20,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
backgroundColor: 'rgba(0,0,0,0.55)',
|
||||
zIndex: 20,
|
||||
},
|
||||
backToChartsText: {
|
||||
color: Palette.white,
|
||||
fontSize: 12,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
import { useIsFocused, useRoute } from '@react-navigation/native'
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { FlatList, Image, Pressable, StyleSheet, View, useWindowDimensions } from 'react-native'
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
Pressable,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
useWindowDimensions,
|
||||
} from 'react-native'
|
||||
import { icons } from '../../assets'
|
||||
import { projectsRef } from '../../config/firebase'
|
||||
import useDataFromRef from '../../hooks/useDataFromRef'
|
||||
@@ -8,12 +16,15 @@ import { useUser } from '../../providers/UserDataProvider'
|
||||
import { Palette } from '../../styles'
|
||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||
import PlaybackItem from './components/PlaybackItem.web'
|
||||
import PlaybackCharts from './components/PlaybackCharts'
|
||||
|
||||
const Playbacks = () => {
|
||||
const { height: windowHeight } = useWindowDimensions()
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const route = useRoute()
|
||||
const focusProjectId = route?.params?.projectId || route?.params?.focusId || null
|
||||
const initialFocusProjectId = route?.params?.projectId || route?.params?.focusId || null
|
||||
const [focusProjectId, setFocusProjectId] = useState(initialFocusProjectId)
|
||||
const [viewMode, setViewMode] = useState(initialFocusProjectId ? 'feed' : 'charts')
|
||||
const userCache = useRef(new Map())
|
||||
const { getUserByUid } = useUser() || {}
|
||||
const isFocused = useIsFocused()
|
||||
@@ -44,9 +55,6 @@ const Playbacks = () => {
|
||||
|
||||
const activePlayback = playbacks?.[activeIndex] || null
|
||||
const activeVideoUrl = activePlayback?.playbackUrl || null
|
||||
const activeTitle = typeof activePlayback?.title === 'string' ? activePlayback.title.trim() : ''
|
||||
|
||||
const activeCreatorName = activePlayback?.userName
|
||||
const backgroundVideoRef = useRef(null)
|
||||
const lastActiveIndexRef = useRef(0)
|
||||
const backgroundSeekPendingRef = useRef(false)
|
||||
@@ -161,7 +169,6 @@ const Playbacks = () => {
|
||||
}
|
||||
}, [activeVideoUrl])
|
||||
|
||||
const triedLoadMoreRef = useRef(0)
|
||||
const hasAppliedFocusRef = useRef(false)
|
||||
const focusLoadAttemptsRef = useRef(0)
|
||||
|
||||
@@ -189,6 +196,27 @@ const Playbacks = () => {
|
||||
}
|
||||
}, [focusIndex, focusProjectId, loadMore, playbacks])
|
||||
|
||||
useEffect(() => {
|
||||
const nextFocusId = route?.params?.projectId || route?.params?.focusId || null
|
||||
setFocusProjectId(nextFocusId)
|
||||
if (nextFocusId) {
|
||||
setViewMode('feed')
|
||||
}
|
||||
}, [route?.params?.focusId, route?.params?.projectId])
|
||||
|
||||
useEffect(() => {
|
||||
if (viewMode !== 'feed') {
|
||||
return
|
||||
}
|
||||
lastActiveIndexRef.current = 0
|
||||
setActiveIndex(0)
|
||||
requestAnimationFrame(() => {
|
||||
try {
|
||||
listRef.current?.scrollToOffset?.({ offset: 0, animated: false })
|
||||
} catch (_e) {}
|
||||
})
|
||||
}, [viewMode])
|
||||
|
||||
// === FlatList (one real page per item) ===
|
||||
// keep active index in sync with scroll
|
||||
const onMomentumScrollEnd = useCallback(
|
||||
@@ -294,6 +322,24 @@ const Playbacks = () => {
|
||||
[activeIndex, handleActiveIndexChange, hasMore, loadMore, playbacks.length, windowHeight]
|
||||
)
|
||||
|
||||
const showFeed = viewMode === 'feed'
|
||||
|
||||
const openFeedForPlayback = useCallback((project) => {
|
||||
const nextId =
|
||||
project && typeof project === 'object'
|
||||
? project?.id || project?.projectId || null
|
||||
: project || null
|
||||
if (!nextId) {
|
||||
return
|
||||
}
|
||||
setFocusProjectId(nextId)
|
||||
setViewMode('feed')
|
||||
}, [])
|
||||
|
||||
if (!showFeed) {
|
||||
return <PlaybackCharts onPlaybackPress={openFeedForPlayback} />
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.screen}>
|
||||
{activeVideoUrl ? (
|
||||
@@ -396,6 +442,14 @@ const Playbacks = () => {
|
||||
</Pressable>
|
||||
</View>
|
||||
)}
|
||||
<Pressable
|
||||
style={styles.backToCharts}
|
||||
hitSlop={12}
|
||||
onPress={() => setViewMode('charts')}
|
||||
accessibilityLabel="Revenir au classement des playbacks"
|
||||
>
|
||||
<Text style={styles.backToChartsText}>Classement</Text>
|
||||
</Pressable>
|
||||
</View>
|
||||
// </Page>
|
||||
)
|
||||
@@ -505,4 +559,19 @@ const styles = StyleSheet.create({
|
||||
fontFamily: FONT_FAMILY.InterMedium,
|
||||
textAlign: 'center',
|
||||
},
|
||||
backToCharts: {
|
||||
position: 'absolute',
|
||||
top: 32,
|
||||
right: 24,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 12,
|
||||
borderRadius: 12,
|
||||
backgroundColor: 'rgba(0,0,0,0.55)',
|
||||
zIndex: 20,
|
||||
},
|
||||
backToChartsText: {
|
||||
color: Palette.white,
|
||||
fontSize: 12,
|
||||
fontFamily: FONT_FAMILY.InterSemiBold,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -0,0 +1,650 @@
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import {
|
||||
FlatList,
|
||||
Image,
|
||||
Platform,
|
||||
ScrollView,
|
||||
StyleSheet,
|
||||
Text,
|
||||
View,
|
||||
} from 'react-native'
|
||||
import { BlurView } from 'expo-blur'
|
||||
import { responsiveHeight } from 'react-native-responsive-dimensions'
|
||||
import { background, icons } from '../../../assets'
|
||||
import BorderGradientButton from '../../../components/BorderGradientButton'
|
||||
import MobileCoinBadge from '../../../components/MobileCoinBadge'
|
||||
import SearchBar from '../../../components/SearchBar'
|
||||
import ShareBtn from '../../../components/ShareBtn/ShareBtn'
|
||||
import firebase, { projectsRef, usersRef } from '../../../config/firebase'
|
||||
import useDataFromRef from '../../../hooks/useDataFromRef'
|
||||
import useSearch from '../../../hooks/useSearch'
|
||||
import Page from '../../../layouts/Page'
|
||||
import ResearchHeader from '../../Library/components/ResearchHeader'
|
||||
import SearchResultsList from '../../Library/components/SearchResultsList'
|
||||
import CreateLyricsHeader from '../../Writing/components/CreateLyricsHeader'
|
||||
import SongCard from '../../HitParade/components/SongCard'
|
||||
import { Palette, Style } from '../../../styles'
|
||||
import { FONT_FAMILY } from '../../../styles/Fonts'
|
||||
|
||||
const allowedPremiumLevels = new Set(['starter', 'pro', 'premium'])
|
||||
|
||||
const normalizePremiumLevel = (value) => {
|
||||
if (typeof value !== 'string') {
|
||||
return null
|
||||
}
|
||||
const normalized = value.trim().toLowerCase()
|
||||
return allowedPremiumLevels.has(normalized) ? normalized : null
|
||||
}
|
||||
|
||||
const chunkArray = (items = [], size = 10) => {
|
||||
const chunks = []
|
||||
for (let i = 0; i < items.length; i += size) {
|
||||
chunks.push(items.slice(i, i + size))
|
||||
}
|
||||
return chunks
|
||||
}
|
||||
|
||||
const PlaybackCharts = ({ onPlaybackPress }) => {
|
||||
const isWeb = Platform.OS === 'web'
|
||||
const [selectedCategory, setSelectedCategory] = useState('Playbacks')
|
||||
const {
|
||||
data: allPlaybacks,
|
||||
loading: allPlaybacksLoading,
|
||||
} = useDataFromRef({
|
||||
ref: projectsRef.where('playbackUrl', '!=', null),
|
||||
format: (docs) => docs.filter((item) => !!item?.playbackUrl),
|
||||
simpleRef: false,
|
||||
listener: true,
|
||||
condition: true,
|
||||
})
|
||||
|
||||
const { data: topMonthPlaybacks, loading: topMonthLoading } = useDataFromRef({
|
||||
ref: projectsRef.where('monthViews', '>', 0).orderBy('monthViews', 'desc').limit(20),
|
||||
format: (docs) => docs.filter((item) => !!item?.playbackUrl),
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: true,
|
||||
})
|
||||
|
||||
const { data: allTimePlaybacks, loading: allTimeLoading } = useDataFromRef({
|
||||
ref: projectsRef.where('views', '>', 0).orderBy('views', 'desc').limit(20),
|
||||
format: (docs) => docs.filter((item) => !!item?.playbackUrl),
|
||||
simpleRef: false,
|
||||
listener: false,
|
||||
condition: true,
|
||||
})
|
||||
|
||||
const playbacksList = useMemo(
|
||||
() => (Array.isArray(allPlaybacks) ? allPlaybacks : []),
|
||||
[allPlaybacks]
|
||||
)
|
||||
|
||||
const topMonthList = useMemo(
|
||||
() => (Array.isArray(topMonthPlaybacks) ? topMonthPlaybacks : []),
|
||||
[topMonthPlaybacks]
|
||||
)
|
||||
|
||||
const allTimeList = useMemo(
|
||||
() => (Array.isArray(allTimePlaybacks) ? allTimePlaybacks : []),
|
||||
[allTimePlaybacks]
|
||||
)
|
||||
|
||||
const {
|
||||
search,
|
||||
setSearch,
|
||||
selected: searchSelected,
|
||||
setSelected: setSearchSelected,
|
||||
users = [],
|
||||
musics = [],
|
||||
playbacks = [],
|
||||
loading: searchLoading,
|
||||
} = useSearch()
|
||||
|
||||
const [dropdownVisible, setDropdownVisible] = useState(false)
|
||||
const searchWrapperRef = useRef(null)
|
||||
|
||||
const toggleSearchSelected = useCallback(
|
||||
(value) => {
|
||||
setSearchSelected((previous) => (previous === value ? null : value))
|
||||
},
|
||||
[setSearchSelected]
|
||||
)
|
||||
|
||||
const musicResults = useMemo(() => (Array.isArray(musics) ? musics : []), [musics])
|
||||
const playbackResults = useMemo(() => (Array.isArray(playbacks) ? playbacks : []), [playbacks])
|
||||
const userResults = useMemo(() => (Array.isArray(users) ? users : []), [users])
|
||||
|
||||
const musicsLoading = searchLoading
|
||||
const playbacksLoading = searchLoading
|
||||
const usersLoading = searchLoading
|
||||
|
||||
const [creatorsById, setCreatorsById] = useState({})
|
||||
|
||||
const requiredUserIds = useMemo(() => {
|
||||
const collected = new Set()
|
||||
const collectUserId = (project) => {
|
||||
const uid = project && typeof project.userId === 'string' ? project.userId.trim() : null
|
||||
if (uid) {
|
||||
collected.add(uid)
|
||||
}
|
||||
}
|
||||
playbacksList.forEach(collectUserId)
|
||||
topMonthList.forEach(collectUserId)
|
||||
allTimeList.forEach(collectUserId)
|
||||
return Array.from(collected)
|
||||
}, [allTimeList, playbacksList, topMonthList])
|
||||
|
||||
const fetchCreatorsByIds = useCallback(
|
||||
async (userIds) => {
|
||||
if (!Array.isArray(userIds) || userIds.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const normalizedIds = userIds
|
||||
.map((id) => (typeof id === 'string' ? id.trim() : null))
|
||||
.filter(Boolean)
|
||||
|
||||
if (normalizedIds.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const pendingIds = new Set(normalizedIds)
|
||||
const nextCreators = {}
|
||||
|
||||
const idChunks = chunkArray(normalizedIds, 10)
|
||||
|
||||
await Promise.all(
|
||||
idChunks.map(async (chunk) => {
|
||||
try {
|
||||
const snapshot = await usersRef
|
||||
.where(firebase.firestore.FieldPath.documentId(), 'in', chunk)
|
||||
.get()
|
||||
|
||||
snapshot.docs.forEach((doc) => {
|
||||
const data = doc.data() || {}
|
||||
nextCreators[doc.id] = {
|
||||
premiumLevel: normalizePremiumLevel(data.premiumLevel),
|
||||
}
|
||||
pendingIds.delete(doc.id)
|
||||
})
|
||||
} catch (error) {
|
||||
console.log('PlaybackCharts: unable to fetch creators', error?.message || error)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
pendingIds.forEach((userId) => {
|
||||
nextCreators[userId] = { premiumLevel: null }
|
||||
})
|
||||
|
||||
if (Object.keys(nextCreators).length > 0) {
|
||||
setCreatorsById((previous) => ({ ...previous, ...nextCreators }))
|
||||
}
|
||||
},
|
||||
[setCreatorsById]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const missingIds = requiredUserIds.filter((id) => !creatorsById[id])
|
||||
if (missingIds.length === 0) {
|
||||
return
|
||||
}
|
||||
fetchCreatorsByIds(missingIds)
|
||||
}, [creatorsById, fetchCreatorsByIds, requiredUserIds])
|
||||
|
||||
const getCreatorLevel = useCallback(
|
||||
(project) => {
|
||||
const uid = project && typeof project.userId === 'string' ? project.userId.trim() : null
|
||||
if (!uid) {
|
||||
return null
|
||||
}
|
||||
return creatorsById?.[uid]?.premiumLevel || null
|
||||
},
|
||||
[creatorsById]
|
||||
)
|
||||
|
||||
const closeDropdown = useCallback(() => {
|
||||
setDropdownVisible(false)
|
||||
setSearchSelected(null)
|
||||
setSearch('')
|
||||
}, [setSearch, setSearchSelected])
|
||||
|
||||
const handleSearchFocus = useCallback(() => {
|
||||
if (isWeb) {
|
||||
setDropdownVisible(true)
|
||||
}
|
||||
}, [isWeb])
|
||||
|
||||
const handleSearchChange = useCallback(
|
||||
(value) => {
|
||||
setSearch(value)
|
||||
if (isWeb) {
|
||||
setDropdownVisible(true)
|
||||
}
|
||||
},
|
||||
[isWeb, setSearch]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isWeb || !dropdownVisible) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const handleClickOutside = (event) => {
|
||||
if (searchWrapperRef.current?.contains?.(event.target)) {
|
||||
return
|
||||
}
|
||||
closeDropdown()
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
document.addEventListener('touchstart', handleClickOutside)
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
document.removeEventListener('touchstart', handleClickOutside)
|
||||
}
|
||||
}, [closeDropdown, dropdownVisible, isWeb])
|
||||
|
||||
const shouldShowResults = isWeb && dropdownVisible
|
||||
const shouldBlurContent = shouldShowResults
|
||||
|
||||
const handlePlaybackSelected = useCallback(
|
||||
(project) => {
|
||||
if (!project?.id) {
|
||||
return
|
||||
}
|
||||
onPlaybackPress?.(project)
|
||||
},
|
||||
[onPlaybackPress]
|
||||
)
|
||||
|
||||
const heroTitle = isWeb ? 'Playbacks coups de coeur' : 'Classements des playbacks'
|
||||
const heroSubtitle = 'Découvre les playbacks les plus écoutés et lance la lecture verticale.'
|
||||
|
||||
const renderHero = () => (
|
||||
<View style={{ gap: 11, maxWidth: 800, width: '100%', alignSelf: 'center' }}>
|
||||
<CreateLyricsHeader gradientProps={{ colors: ['#F94697', '#7023F7'] }}>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 16,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueBold,
|
||||
}}
|
||||
>
|
||||
{heroTitle}
|
||||
</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 12,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.InterRegular,
|
||||
}}
|
||||
>
|
||||
{heroSubtitle}
|
||||
</Text>
|
||||
</CreateLyricsHeader>
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderPlaybacksMobile = () => (
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
gap: 10,
|
||||
paddingBottom: responsiveHeight(20),
|
||||
}}
|
||||
data={playbacksList}
|
||||
keyExtractor={(item) => item.id}
|
||||
refreshing={allPlaybacksLoading}
|
||||
renderItem={({ item }) => (
|
||||
<SongCard
|
||||
rank={null}
|
||||
title={item?.title || 'Sans titre'}
|
||||
artist={item?.userName}
|
||||
coverUrl={item?.coverUrl || null}
|
||||
subscriptionLevel={getCreatorLevel(item)}
|
||||
onPress={() => handlePlaybackSelected(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
const renderTopMonthMobile = () => (
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
gap: 10,
|
||||
paddingBottom: responsiveHeight(20),
|
||||
}}
|
||||
data={topMonthList}
|
||||
keyExtractor={(item) => item.id}
|
||||
refreshing={topMonthLoading}
|
||||
renderItem={({ item, index }) => (
|
||||
<SongCard
|
||||
rank={index + 1}
|
||||
title={item?.title || 'Sans titre'}
|
||||
artist={item?.userName}
|
||||
coverUrl={item?.coverUrl || null}
|
||||
subscriptionLevel={getCreatorLevel(item)}
|
||||
onPress={() => handlePlaybackSelected(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
const renderAllTimeMobile = () => (
|
||||
<FlatList
|
||||
contentContainerStyle={{
|
||||
gap: 10,
|
||||
paddingBottom: responsiveHeight(20),
|
||||
}}
|
||||
data={allTimeList}
|
||||
keyExtractor={(item) => item.id}
|
||||
refreshing={allTimeLoading}
|
||||
renderItem={({ item, index }) => (
|
||||
<SongCard
|
||||
rank={index + 1}
|
||||
title={item?.title || 'Sans titre'}
|
||||
artist={item?.userName}
|
||||
coverUrl={item?.coverUrl || null}
|
||||
subscriptionLevel={getCreatorLevel(item)}
|
||||
onPress={() => handlePlaybackSelected(item)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
const renderPlaybacksList = () => (
|
||||
<View style={{ gap: 10 }}>
|
||||
{playbacksList.map((item) => (
|
||||
<SongCard
|
||||
key={item.id}
|
||||
rank={null}
|
||||
title={item?.title || 'Sans titre'}
|
||||
artist={item?.userName}
|
||||
coverUrl={item?.coverUrl || null}
|
||||
subscriptionLevel={getCreatorLevel(item)}
|
||||
onPress={() => handlePlaybackSelected(item)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderTopMonthColumns = () => (
|
||||
<View style={{ gap: 10 }}>
|
||||
{topMonthList.map((item, index) => (
|
||||
<SongCard
|
||||
key={item.id}
|
||||
rank={index + 1}
|
||||
title={item?.title || 'Sans titre'}
|
||||
artist={item?.userName}
|
||||
coverUrl={item?.coverUrl || null}
|
||||
subscriptionLevel={getCreatorLevel(item)}
|
||||
onPress={() => handlePlaybackSelected(item)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderAllTimeColumns = () => (
|
||||
<View style={{ gap: 10 }}>
|
||||
{allTimeList.map((item, index) => (
|
||||
<SongCard
|
||||
key={item.id}
|
||||
rank={index + 1}
|
||||
title={item?.title || 'Sans titre'}
|
||||
artist={item?.userName}
|
||||
coverUrl={item?.coverUrl || null}
|
||||
subscriptionLevel={getCreatorLevel(item)}
|
||||
onPress={() => handlePlaybackSelected(item)}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
)
|
||||
|
||||
const renderWebContent = () => (
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
flexGrow: 1,
|
||||
gap: 24,
|
||||
paddingBottom: responsiveHeight(20),
|
||||
}}
|
||||
showsVerticalScrollIndicator={false}
|
||||
style={{ flex: 1, position: 'relative', zIndex: 5 }}
|
||||
>
|
||||
{renderHero()}
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
gap: 16,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<View style={{ flex: 1, gap: 12 }}>
|
||||
<BlurView
|
||||
intensity={50}
|
||||
tint="dark"
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueBold,
|
||||
}}
|
||||
>
|
||||
Playbacks
|
||||
</Text>
|
||||
</BlurView>
|
||||
{renderPlaybacksList()}
|
||||
</View>
|
||||
<View style={{ flex: 1, gap: 12 }}>
|
||||
<BlurView
|
||||
intensity={50}
|
||||
tint="dark"
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueBold,
|
||||
}}
|
||||
>
|
||||
Top du mois
|
||||
</Text>
|
||||
</BlurView>
|
||||
{renderTopMonthColumns()}
|
||||
</View>
|
||||
<View style={{ flex: 1, gap: 12 }}>
|
||||
<BlurView
|
||||
intensity={50}
|
||||
tint="dark"
|
||||
style={{
|
||||
alignSelf: 'flex-start',
|
||||
borderRadius: 10,
|
||||
overflow: 'hidden',
|
||||
paddingHorizontal: 20,
|
||||
paddingVertical: 10,
|
||||
}}
|
||||
>
|
||||
<Text
|
||||
style={{
|
||||
fontSize: 20,
|
||||
color: Palette.white,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueBold,
|
||||
}}
|
||||
>
|
||||
All Time
|
||||
</Text>
|
||||
</BlurView>
|
||||
{renderAllTimeColumns()}
|
||||
</View>
|
||||
</View>
|
||||
</ScrollView>
|
||||
)
|
||||
|
||||
const renderMobileContent = () => (
|
||||
<View style={{ flex: 1, gap: 24 }}>
|
||||
{renderHero()}
|
||||
<View
|
||||
style={{
|
||||
...Style.containerRow,
|
||||
justifyContent: 'space-between',
|
||||
width: 'auto',
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
{['Playbacks', 'Top du mois', 'All Time'].map((item, index) => (
|
||||
<BorderGradientButton
|
||||
key={index}
|
||||
title={item}
|
||||
onPress={() => setSelectedCategory(item)}
|
||||
tint={selectedCategory === item ? 'light' : 'dark'}
|
||||
containerStyle={{ flex: 1 }}
|
||||
titleStyle={{
|
||||
fontSize: 16,
|
||||
fontFamily: FONT_FAMILY.HelveticaNeueBold,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
|
||||
{selectedCategory === 'Playbacks' && renderPlaybacksMobile()}
|
||||
{selectedCategory === 'Top du mois' && renderTopMonthMobile()}
|
||||
{selectedCategory === 'All Time' && renderAllTimeMobile()}
|
||||
</View>
|
||||
)
|
||||
|
||||
return (
|
||||
<Page
|
||||
backgroundImg={isWeb ? background.playbackBG2 : background.playbackBG}
|
||||
headerType="NONE"
|
||||
width="100%"
|
||||
maxWidth={1200}
|
||||
blurIntensity={isWeb ? 0 : 0}
|
||||
showCoin={false}
|
||||
>
|
||||
{!isWeb ? (
|
||||
<View
|
||||
style={{
|
||||
width: '100%',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
marginBottom: 4,
|
||||
}}
|
||||
>
|
||||
<MobileCoinBadge style={{ flexShrink: 1 }} />
|
||||
<ShareBtn label="Partager" style={{ flexShrink: 0 }} />
|
||||
</View>
|
||||
) : null}
|
||||
<View style={{ gap: 12, flex: 1 }}>
|
||||
<Image
|
||||
source={icons.musicLandLogo}
|
||||
style={{ alignSelf: 'center', height: 150, resizeMode: 'contain' }}
|
||||
/>
|
||||
|
||||
{isWeb && (
|
||||
<View
|
||||
ref={searchWrapperRef}
|
||||
style={{
|
||||
position: 'relative',
|
||||
alignSelf: 'center',
|
||||
width: '65%',
|
||||
maxWidth: 520,
|
||||
minWidth: 360,
|
||||
zIndex: dropdownVisible ? 40 : 1,
|
||||
overflow: 'visible',
|
||||
}}
|
||||
>
|
||||
<SearchBar
|
||||
textInputProps={{
|
||||
value: search,
|
||||
onChangeText: handleSearchChange,
|
||||
autoFocus: false,
|
||||
onFocus: handleSearchFocus,
|
||||
}}
|
||||
/>
|
||||
{shouldShowResults && (
|
||||
<View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 60,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 50,
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<View
|
||||
style={{
|
||||
gap: 14,
|
||||
borderRadius: 18,
|
||||
padding: 16,
|
||||
backgroundColor: Palette.ultraLightWhite,
|
||||
...Style.defaultBorder,
|
||||
width: '100%',
|
||||
zIndex: 50,
|
||||
elevation: 12,
|
||||
}}
|
||||
>
|
||||
<ResearchHeader
|
||||
onPress={toggleSearchSelected}
|
||||
selected={searchSelected}
|
||||
showSearchBar={false}
|
||||
/>
|
||||
<SearchResultsList
|
||||
selected={searchSelected}
|
||||
musics={musicResults}
|
||||
musicsLoading={musicsLoading}
|
||||
playbacks={playbackResults}
|
||||
playbacksLoading={playbacksLoading}
|
||||
users={userResults}
|
||||
usersLoading={usersLoading}
|
||||
onResultSelected={closeDropdown}
|
||||
style={{ flex: 0, maxHeight: 420 }}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={{ flex: 1, position: 'relative' }}>
|
||||
{shouldBlurContent && (
|
||||
<BlurView
|
||||
intensity={35}
|
||||
tint="dark"
|
||||
style={[
|
||||
StyleSheet.absoluteFillObject,
|
||||
{
|
||||
zIndex: 10,
|
||||
borderRadius: 18,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.25)',
|
||||
overflow: 'hidden',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isWeb ? renderWebContent() : renderMobileContent()}
|
||||
</View>
|
||||
</View>
|
||||
</Page>
|
||||
)
|
||||
}
|
||||
|
||||
export default PlaybackCharts
|
||||
Reference in New Issue
Block a user