feat: musicland

This commit is contained in:
2025-12-04 16:11:48 +01:00
parent 2985f4fa2f
commit 0fab276d3e
3 changed files with 378 additions and 150 deletions
+376 -148
View File
@@ -1,20 +1,30 @@
import React, { useCallback, useMemo, useState } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import {
FlatList,
Image,
Platform,
Pressable,
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 SearchBar from "../../components/SearchBar";
import ShareBtn from "../../components/ShareBtn/ShareBtn";
import { projectsRef } from "../../config/firebase";
import useDataFromRef from "../../hooks/useDataFromRef";
import useNavigateToMusicDetails from "../../hooks/useNavigateToMusicDetails";
import useSearch from "../../hooks/useSearch";
import Page from "../../layouts/Page";
import { Routes } from "../../navigation";
import { navigate } from "../../navigation/NavigationService";
@@ -22,31 +32,33 @@ import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts";
import CreateLyricsHeader from "../Writing/components/CreateLyricsHeader";
import MobileCoinBadge from "../../components/MobileCoinBadge";
import ResearchHeader from "../Library/components/ResearchHeader";
import SearchResultsList from "../Library/components/SearchResultsList";
import PlaybacksCard from "./components/PlaybacksCard";
import SongCard from "./components/SongCard";
import BorderGradient from "../../components/BorderGradient/BorderGradient.web";
const HitParade = () => {
const [selected, setSelected] = useState("Chansons");
const [selectedCategory, setSelectedCategory] = useState("Chansons");
const navigateToMusicDetails = useNavigateToMusicDetails();
const { data: topSongs } = useDataFromRef({
ref: projectsRef.where("views", ">", 0).orderBy("views", "desc").limit(10),
ref: projectsRef.where("views", ">", 0).orderBy("views", "desc").limit(20),
simpleRef: false,
listener: true,
condition: true,
});
const { data: topPlaybacks } = useDataFromRef({
ref: projectsRef.where("views", ">", 0).orderBy("views", "desc").limit(50),
format: (docs) => docs.filter((d) => d?.playbackUrl != null).slice(0, 10),
ref: projectsRef.where("views", ">", 0).orderBy("views", "desc").limit(60),
format: (docs) => docs.filter((d) => d?.playbackUrl != null).slice(0, 20),
simpleRef: false,
listener: true,
condition: true,
});
// Découpe en 2 colonnes (pour le web uniquement)
const [songsCol1, songsCol2] = useMemo(() => {
const arr = Array.isArray(topSongs) ? topSongs : [];
return [arr.slice(0, 5), arr.slice(5)];
}, [topSongs]);
const songsList = useMemo(
() => (Array.isArray(topSongs) ? topSongs : []),
[topSongs]
);
const [playbacksCol1, playbacksCol2] = useMemo(() => {
const arr = Array.isArray(topPlaybacks) ? topPlaybacks : [];
@@ -60,7 +72,7 @@ const HitParade = () => {
project?.coverUrl,
];
const uri = candidates.find(
(value) => typeof value === "string" && value.trim().length > 0,
(value) => typeof value === "string" && value.trim().length > 0
);
return uri || null;
}, []);
@@ -112,103 +124,147 @@ const HitParade = () => {
/>
);
const renderSongsWeb = () => (
<ScrollView
contentContainerStyle={{
gap: 10,
paddingBottom: responsiveHeight(20),
}}
>
<View style={{ flexDirection: "row", gap: 10, alignItems: "flex-start" }}>
{/* Colonne gauche (1..5) */}
<View style={{ flex: 1, gap: 10 }}>
{songsCol1.map((item, idx) => (
<SongCard
key={item.id}
rank={idx + 1}
title={item?.title || "Sans titre"}
artist={"MusicLand"}
coverUrl={item?.coverUrl || null}
onPress={() =>
navigateToMusicDetails({
projectId: item.id,
songUrl: item?.songUrl,
project: item,
})
}
/>
))}
</View>
{/* Colonne droite (6..N) */}
{songsCol2.length > 0 && (
<View style={{ flex: 1, gap: 10 }}>
{songsCol2.map((item, idx) => (
<SongCard
key={item.id}
rank={5 + idx + 1}
title={item?.title || "Sans titre"}
artist={"MusicLand"}
coverUrl={item?.coverUrl || null}
onPress={() =>
navigateToMusicDetails({
projectId: item.id,
songUrl: item?.songUrl,
project: item,
})
}
/>
))}
</View>
)}
</View>
</ScrollView>
const renderSongList = () => (
<View style={{ gap: 10 }}>
{songsList.map((item, idx) => (
<SongCard
key={item.id}
rank={idx + 1}
title={item?.title || "Sans titre"}
artist={item?.userName}
coverUrl={item?.coverUrl || null}
onPress={() =>
navigateToMusicDetails({
projectId: item.id,
songUrl: item?.songUrl,
project: item,
})
}
/>
))}
</View>
);
const renderPlaybacksWeb = () => (
<ScrollView
contentContainerStyle={{
gap: 10,
paddingBottom: responsiveHeight(20),
}}
>
<View style={{ flexDirection: "row", gap: 10, alignItems: "flex-start" }}>
{/* Colonne gauche (1..5) */}
const renderPlaybackColumns = () => (
<View style={{ flexDirection: "row", gap: 10, alignItems: "flex-start" }}>
<View style={{ flex: 1, gap: 10 }}>
{playbacksCol1.map((item, idx) => (
<PlaybacksCard
key={item.id}
rank={idx + 1}
title={item?.title || "Sans titre"}
artist={item?.userName}
thumbnailUrl={resolvePlaybackThumbnail(item)}
onPress={() => navigate(Routes.Playbacks, { projectId: item.id })}
/>
))}
</View>
{playbacksCol2.length > 0 && (
<View style={{ flex: 1, gap: 10 }}>
{playbacksCol1.map((item, idx) => (
{playbacksCol2.map((item, idx) => (
<PlaybacksCard
key={item.id}
rank={idx + 1}
rank={5 + idx + 1}
title={item?.title || "Sans titre"}
artist={"MusicLand"}
artist={item?.userName}
thumbnailUrl={resolvePlaybackThumbnail(item)}
onPress={() => navigate(Routes.Playbacks, { projectId: item.id })}
/>
))}
</View>
{/* Colonne droite (6..N) */}
{playbacksCol2.length > 0 && (
<View style={{ flex: 1, gap: 10 }}>
{playbacksCol2.map((item, idx) => (
<PlaybacksCard
key={item.id}
rank={5 + idx + 1}
title={item?.title || "Sans titre"}
artist={"MusicLand"}
thumbnailUrl={resolvePlaybackThumbnail(item)}
onPress={() =>
navigate(Routes.Playbacks, { projectId: item.id })
}
/>
))}
</View>
)}
</View>
</ScrollView>
)}
</View>
);
const isWeb = Platform.OS === "web";
const {
search,
setSearch,
selected: searchSelected,
setSelected: setSearchSelected,
users = [],
musics = [],
playbacks = [],
loading: searchLoading,
} = useSearch();
const [dropdownVisible, setDropdownVisible] = useState(false);
const searchWrapperRef = useRef(null);
const hasSearchQuery = search.trim().length > 0;
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 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 && hasSearchQuery;
const handlePlayRandomSong = useCallback(() => {
const arr = Array.isArray(topSongs) ? topSongs : [];
@@ -226,12 +282,162 @@ const HitParade = () => {
});
}, [navigateToMusicDetails, topSongs]);
const categoryLabel =
selectedCategory === "Playback" ? "playbacks" : "chansons";
const heroTitle = isWeb
? "Coups de coeur des utilisateurs"
: `Coups de coeur des utilisateurs catégorie ${selectedCategory}`;
const heroSubtitle = isWeb
? "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 = () => (
<View
style={{ gap: 11, maxWidth: 800, width: "100%", alignSelf: "center" }}
>
{/* <Pressable onPress={handlePlayRandomSong}>
<Image
source={icons.play}
style={{ alignSelf: "center", marginVertical: 10 }}
/>
</Pressable> */}
<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 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,
}}
>
{/* <BorderGradient> */}
<Text
style={{
fontSize: 20,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
}}
>
Chansons
</Text>
{/* </BorderGradient> */}
</BlurView>
{renderSongList()}
</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,
}}
>
Playbacks
</Text>
{/* </BorderGradient> */}
</BlurView>
{renderPlaybackColumns()}
</View>
</View>
</ScrollView>
);
const renderMobileContent = () => (
<View style={{ flex: 1, gap: 24 }}>
{renderHero()}
<View
style={{
...Style.containerRow,
justifyContent: "space-between",
width: "auto",
gap: 12,
}}
>
{["Chansons", "Playback"].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 === "Chansons" && renderSongsMobile()}
{selectedCategory === "Playback" && renderPlaybacksMobile()}
{/* "Clips" pourra reprendre la même logique si tu le réactives */}
</View>
);
return (
<Page
backgroundImg={isWeb ? background.hitParadeBG2 : background.hitParadeBG}
headerType="NONE"
width="100%"
maxWidth={800}
maxWidth={1200}
blurIntensity={isWeb ? 0 : 0}
showCoin={false}
>
@@ -255,67 +461,89 @@ const HitParade = () => {
source={icons.musicLandLogo}
style={{ alignSelf: "center", height: 150, resizeMode: "contain" }}
/>
<View style={{ flex: 1, gap: 24 }}>
<View style={{ gap: 11 }}>
<Pressable onPress={handlePlayRandomSong}>
<Image
source={icons.play}
style={{ alignSelf: "center", marginVertical: 10 }}
/>
</Pressable>
<CreateLyricsHeader
gradientProps={{ colors: ["#F94697", "#7023F7"] }}
>
<Text
style={{
fontSize: 16,
color: Palette.white,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
}}
>
Coups de coeur des utilisateurs catégorie {selected}
</Text>
<Text
style={{
fontSize: 12,
color: Palette.white,
fontFamily: FONT_FAMILY.InterRegular,
}}
>
Les 3 {selected} les plus likées du mois reçoivent des{"\n"}
tokens.
</Text>
</CreateLyricsHeader>
</View>
{isWeb && (
<View
ref={searchWrapperRef}
style={{
...Style.containerRow,
justifyContent: "space-between",
width: "auto",
gap: 12,
position: "relative",
alignSelf: "center",
width: "65%",
maxWidth: 520,
minWidth: 360,
zIndex: dropdownVisible ? 40 : 1,
overflow: "visible",
}}
>
{["Chansons", "Playback"].map((item, index) => (
<BorderGradientButton
key={index}
title={item}
onPress={() => setSelected(item)}
tint={selected === item ? "light" : "dark"}
containerStyle={{ flex: 1 }}
titleStyle={{
fontSize: 16,
fontFamily: FONT_FAMILY.HelveticaNeueBold,
<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>
)}
{selected === "Chansons" &&
(isWeb ? renderSongsWeb() : renderSongsMobile())}
{selected === "Playback" &&
(isWeb ? renderPlaybacksWeb() : renderPlaybacksMobile())}
{/* "Clips" pourra reprendre la même logique si tu le réactives */}
<View style={{ flex: 1, position: "relative" }}>
{shouldBlurContent && (
<BlurView
intensity={35}
tint="dark"
style={[
StyleSheet.absoluteFillObject,
{
zIndex: 10,
backgroundColor: "rgba(0, 0, 0, 0.25)",
},
]}
/>
)}
{isWeb ? renderWebContent() : renderMobileContent()}
</View>
</View>
</Page>
@@ -20,7 +20,7 @@ const PlaybacksCard = ({
return (
<Pressable onPress={onPress}>
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS === "web" ? 40 : Platform.OS !== "ios" ? 10 : 20}
tint="dark"
style={{
...Style.containerSpaceBetween,
+1 -1
View File
@@ -44,7 +44,7 @@ const SongCard = ({
/>
)}
<BlurView
intensity={Platform.OS !== "ios" ? 10 : 20}
intensity={Platform.OS === "web" ? 40 : Platform.OS !== "ios" ? 10 : 20}
style={{
flex: 1,
borderRadius: 12,