feat: playbacks leaderboard
This commit is contained in:
@@ -64,6 +64,14 @@ const HitParade = () => {
|
|||||||
condition: true,
|
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 songsList = useMemo(() => (Array.isArray(allSongs) ? allSongs : []), [allSongs])
|
||||||
|
|
||||||
const topMonthList = useMemo(
|
const topMonthList = useMemo(
|
||||||
@@ -71,6 +79,11 @@ const HitParade = () => {
|
|||||||
[topMonthSongs]
|
[topMonthSongs]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const allTimeList = useMemo(
|
||||||
|
() => (Array.isArray(allTimeSongs) ? allTimeSongs : []),
|
||||||
|
[allTimeSongs]
|
||||||
|
)
|
||||||
|
|
||||||
// === RENDUS PAR PLATEFORME ===
|
// === RENDUS PAR PLATEFORME ===
|
||||||
const renderSongsMobile = () => (
|
const renderSongsMobile = () => (
|
||||||
<FlatList
|
<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 = () => (
|
const renderSongList = () => (
|
||||||
<View style={{ gap: 10 }}>
|
<View style={{ gap: 10 }}>
|
||||||
{songsList.map((item) => (
|
{songsList.map((item) => (
|
||||||
@@ -168,6 +207,27 @@ const HitParade = () => {
|
|||||||
</View>
|
</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 isWeb = Platform.OS === 'web'
|
||||||
const {
|
const {
|
||||||
search,
|
search,
|
||||||
@@ -205,8 +265,9 @@ const HitParade = () => {
|
|||||||
}
|
}
|
||||||
songsList.forEach(collectUserId)
|
songsList.forEach(collectUserId)
|
||||||
topMonthList.forEach(collectUserId)
|
topMonthList.forEach(collectUserId)
|
||||||
|
allTimeList.forEach(collectUserId)
|
||||||
return Array.from(collected)
|
return Array.from(collected)
|
||||||
}, [songsList, topMonthList])
|
}, [songsList, topMonthList, allTimeList])
|
||||||
|
|
||||||
const fetchCreatorsByIds = useCallback(
|
const fetchCreatorsByIds = useCallback(
|
||||||
async (userIds) => {
|
async (userIds) => {
|
||||||
@@ -455,6 +516,32 @@ const HitParade = () => {
|
|||||||
</BlurView>
|
</BlurView>
|
||||||
{renderTopMonthColumns()}
|
{renderTopMonthColumns()}
|
||||||
</View>
|
</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>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
)
|
)
|
||||||
@@ -470,7 +557,7 @@ const HitParade = () => {
|
|||||||
gap: 12,
|
gap: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{['Chansons', 'Top du mois'].map((item, index) => (
|
{['Chansons', 'Top du mois', 'All Time'].map((item, index) => (
|
||||||
<BorderGradientButton
|
<BorderGradientButton
|
||||||
key={index}
|
key={index}
|
||||||
title={item}
|
title={item}
|
||||||
@@ -487,6 +574,7 @@ const HitParade = () => {
|
|||||||
|
|
||||||
{selectedCategory === 'Chansons' && renderSongsMobile()}
|
{selectedCategory === 'Chansons' && renderSongsMobile()}
|
||||||
{selectedCategory === 'Top du mois' && renderTopMonthMobile()}
|
{selectedCategory === 'Top du mois' && renderTopMonthMobile()}
|
||||||
|
{selectedCategory === 'All Time' && renderAllTimeMobile()}
|
||||||
{/* "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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
import { useIsFocused, useRoute } from '@react-navigation/native'
|
import { useIsFocused, useRoute } from '@react-navigation/native'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
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 Carousel from 'react-native-reanimated-carousel'
|
||||||
import { responsiveHeight } from 'react-native-responsive-dimensions'
|
import { responsiveHeight } from 'react-native-responsive-dimensions'
|
||||||
import { projectsRef } from '../../config/firebase'
|
import { projectsRef } from '../../config/firebase'
|
||||||
import useDataFromRef from '../../hooks/useDataFromRef'
|
import useDataFromRef from '../../hooks/useDataFromRef'
|
||||||
import PlaybackItem from './components/PlaybackItem'
|
import PlaybackItem from './components/PlaybackItem'
|
||||||
|
import PlaybackCharts from './components/PlaybackCharts'
|
||||||
|
import { Palette } from '../../styles'
|
||||||
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||||
|
|
||||||
const Playbacks = () => {
|
const Playbacks = () => {
|
||||||
const [activeIndex, setActiveIndex] = useState(0)
|
const [activeIndex, setActiveIndex] = useState(0)
|
||||||
const userCache = useRef(new Map())
|
const userCache = useRef(new Map())
|
||||||
const isFocused = useIsFocused()
|
const isFocused = useIsFocused()
|
||||||
const route = useRoute()
|
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({
|
const { data: rawPlaybacks = [], loadMore } = useDataFromRef({
|
||||||
ref: projectsRef.where('playbackUrl', '!=', null),
|
ref: projectsRef.where('playbackUrl', '!=', null),
|
||||||
simpleRef: false,
|
simpleRef: false,
|
||||||
@@ -75,6 +80,39 @@ const Playbacks = () => {
|
|||||||
}
|
}
|
||||||
}, [focusIndex, focusProjectId, loadMore, 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 (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
<View style={{ flex: 1, backgroundColor: 'black' }}>
|
||||||
<Carousel
|
<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>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Playbacks
|
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 { useIsFocused, useRoute } from '@react-navigation/native'
|
||||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
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 { icons } from '../../assets'
|
||||||
import { projectsRef } from '../../config/firebase'
|
import { projectsRef } from '../../config/firebase'
|
||||||
import useDataFromRef from '../../hooks/useDataFromRef'
|
import useDataFromRef from '../../hooks/useDataFromRef'
|
||||||
@@ -8,12 +16,15 @@ import { useUser } from '../../providers/UserDataProvider'
|
|||||||
import { Palette } from '../../styles'
|
import { Palette } from '../../styles'
|
||||||
import { FONT_FAMILY } from '../../styles/Fonts'
|
import { FONT_FAMILY } from '../../styles/Fonts'
|
||||||
import PlaybackItem from './components/PlaybackItem.web'
|
import PlaybackItem from './components/PlaybackItem.web'
|
||||||
|
import PlaybackCharts from './components/PlaybackCharts'
|
||||||
|
|
||||||
const Playbacks = () => {
|
const Playbacks = () => {
|
||||||
const { height: windowHeight } = useWindowDimensions()
|
const { height: windowHeight } = useWindowDimensions()
|
||||||
const [activeIndex, setActiveIndex] = useState(0)
|
const [activeIndex, setActiveIndex] = useState(0)
|
||||||
const route = useRoute()
|
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 userCache = useRef(new Map())
|
||||||
const { getUserByUid } = useUser() || {}
|
const { getUserByUid } = useUser() || {}
|
||||||
const isFocused = useIsFocused()
|
const isFocused = useIsFocused()
|
||||||
@@ -44,9 +55,6 @@ const Playbacks = () => {
|
|||||||
|
|
||||||
const activePlayback = playbacks?.[activeIndex] || null
|
const activePlayback = playbacks?.[activeIndex] || null
|
||||||
const activeVideoUrl = activePlayback?.playbackUrl || null
|
const activeVideoUrl = activePlayback?.playbackUrl || null
|
||||||
const activeTitle = typeof activePlayback?.title === 'string' ? activePlayback.title.trim() : ''
|
|
||||||
|
|
||||||
const activeCreatorName = activePlayback?.userName
|
|
||||||
const backgroundVideoRef = useRef(null)
|
const backgroundVideoRef = useRef(null)
|
||||||
const lastActiveIndexRef = useRef(0)
|
const lastActiveIndexRef = useRef(0)
|
||||||
const backgroundSeekPendingRef = useRef(false)
|
const backgroundSeekPendingRef = useRef(false)
|
||||||
@@ -161,7 +169,6 @@ const Playbacks = () => {
|
|||||||
}
|
}
|
||||||
}, [activeVideoUrl])
|
}, [activeVideoUrl])
|
||||||
|
|
||||||
const triedLoadMoreRef = useRef(0)
|
|
||||||
const hasAppliedFocusRef = useRef(false)
|
const hasAppliedFocusRef = useRef(false)
|
||||||
const focusLoadAttemptsRef = useRef(0)
|
const focusLoadAttemptsRef = useRef(0)
|
||||||
|
|
||||||
@@ -189,6 +196,27 @@ const Playbacks = () => {
|
|||||||
}
|
}
|
||||||
}, [focusIndex, focusProjectId, loadMore, 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) ===
|
// === FlatList (one real page per item) ===
|
||||||
// keep active index in sync with scroll
|
// keep active index in sync with scroll
|
||||||
const onMomentumScrollEnd = useCallback(
|
const onMomentumScrollEnd = useCallback(
|
||||||
@@ -294,6 +322,24 @@ const Playbacks = () => {
|
|||||||
[activeIndex, handleActiveIndexChange, hasMore, loadMore, playbacks.length, windowHeight]
|
[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 (
|
return (
|
||||||
<View style={styles.screen}>
|
<View style={styles.screen}>
|
||||||
{activeVideoUrl ? (
|
{activeVideoUrl ? (
|
||||||
@@ -396,6 +442,14 @@ const Playbacks = () => {
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
<Pressable
|
||||||
|
style={styles.backToCharts}
|
||||||
|
hitSlop={12}
|
||||||
|
onPress={() => setViewMode('charts')}
|
||||||
|
accessibilityLabel="Revenir au classement des playbacks"
|
||||||
|
>
|
||||||
|
<Text style={styles.backToChartsText}>Classement</Text>
|
||||||
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
// </Page>
|
// </Page>
|
||||||
)
|
)
|
||||||
@@ -505,4 +559,19 @@ const styles = StyleSheet.create({
|
|||||||
fontFamily: FONT_FAMILY.InterMedium,
|
fontFamily: FONT_FAMILY.InterMedium,
|
||||||
textAlign: 'center',
|
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