feat: playbacks leaderboard

This commit is contained in:
2026-01-14 14:41:25 +01:00
parent c36cc5f020
commit f7992385d2
4 changed files with 881 additions and 10 deletions
+90 -2
View File
@@ -64,6 +64,14 @@ const HitParade = () => {
condition: true,
})
const { data: allTimeSongs } = useDataFromRef({
ref: projectsRef.where('views', '>', 0).orderBy('views', 'desc').limit(20),
format: (docs) => docs.filter((item) => item?.hasPlayback !== true),
simpleRef: false,
listener: false,
condition: true,
})
const songsList = useMemo(() => (Array.isArray(allSongs) ? allSongs : []), [allSongs])
const topMonthList = useMemo(
@@ -71,6 +79,11 @@ const HitParade = () => {
[topMonthSongs]
)
const allTimeList = useMemo(
() => (Array.isArray(allTimeSongs) ? allTimeSongs : []),
[allTimeSongs]
)
// === RENDUS PAR PLATEFORME ===
const renderSongsMobile = () => (
<FlatList
@@ -125,6 +138,32 @@ const HitParade = () => {
/>
)
const renderAllTimeMobile = () => (
<FlatList
contentContainerStyle={{
gap: 10,
paddingBottom: responsiveHeight(20),
}}
data={allTimeList}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<SongCard
rank={index + 1}
title={item?.title || 'Sans titre'}
artist={item?.userName}
coverUrl={item?.coverUrl || null}
subscriptionLevel={getCreatorLevel(item)}
onPress={() =>
navigateToMusicDetails({
projectId: item?.id,
songUrl: item?.songUrl,
})
}
/>
)}
/>
)
const renderSongList = () => (
<View style={{ gap: 10 }}>
{songsList.map((item) => (
@@ -168,6 +207,27 @@ const HitParade = () => {
</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={() =>
navigateToMusicDetails({
projectId: item?.id,
songUrl: item?.songUrl,
})
}
/>
))}
</View>
)
const isWeb = Platform.OS === 'web'
const {
search,
@@ -205,8 +265,9 @@ const HitParade = () => {
}
songsList.forEach(collectUserId)
topMonthList.forEach(collectUserId)
allTimeList.forEach(collectUserId)
return Array.from(collected)
}, [songsList, topMonthList])
}, [songsList, topMonthList, allTimeList])
const fetchCreatorsByIds = useCallback(
async (userIds) => {
@@ -455,6 +516,32 @@ const HitParade = () => {
</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,
}}
>
{/* <BorderGradient> */}
<Text
style={{
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
}}
>
All Time
</Text>
{/* </BorderGradient> */}
</BlurView>
{renderAllTimeColumns()}
</View>
</View>
</ScrollView>
)
@@ -470,7 +557,7 @@ const HitParade = () => {
gap: 12,
}}
>
{['Chansons', 'Top du mois'].map((item, index) => (
{['Chansons', 'Top du mois', 'All Time'].map((item, index) => (
<BorderGradientButton
key={index}
title={item}
@@ -487,6 +574,7 @@ const HitParade = () => {
{selectedCategory === 'Chansons' && renderSongsMobile()}
{selectedCategory === 'Top du mois' && renderTopMonthMobile()}
{selectedCategory === 'All Time' && renderAllTimeMobile()}
{/* "Clips" pourra reprendre la même logique si tu le réactives */}
</View>
)
+66 -2
View File
@@ -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,
},
})
+75 -6
View File
@@ -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