From 0fab276d3eda20decda09ed4b6a6c32ad2d874d5 Mon Sep 17 00:00:00 2001 From: leon-morival Date: Thu, 4 Dec 2025 16:11:48 +0100 Subject: [PATCH] feat: musicland --- src/screens/HitParade/HitParade.js | 524 +++++++++++++----- .../HitParade/components/PlaybacksCard.js | 2 +- src/screens/HitParade/components/SongCard.js | 2 +- 3 files changed, 378 insertions(+), 150 deletions(-) diff --git a/src/screens/HitParade/HitParade.js b/src/screens/HitParade/HitParade.js index e5884e1..90cb36a 100644 --- a/src/screens/HitParade/HitParade.js +++ b/src/screens/HitParade/HitParade.js @@ -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 = () => ( - - - {/* Colonne gauche (1..5) */} - - {songsCol1.map((item, idx) => ( - - navigateToMusicDetails({ - projectId: item.id, - songUrl: item?.songUrl, - project: item, - }) - } - /> - ))} - - {/* Colonne droite (6..N) */} - - {songsCol2.length > 0 && ( - - {songsCol2.map((item, idx) => ( - - navigateToMusicDetails({ - projectId: item.id, - songUrl: item?.songUrl, - project: item, - }) - } - /> - ))} - - )} - - + const renderSongList = () => ( + + {songsList.map((item, idx) => ( + + navigateToMusicDetails({ + projectId: item.id, + songUrl: item?.songUrl, + project: item, + }) + } + /> + ))} + ); - const renderPlaybacksWeb = () => ( - - - {/* Colonne gauche (1..5) */} + const renderPlaybackColumns = () => ( + + + {playbacksCol1.map((item, idx) => ( + navigate(Routes.Playbacks, { projectId: item.id })} + /> + ))} + + + {playbacksCol2.length > 0 && ( - {playbacksCol1.map((item, idx) => ( + {playbacksCol2.map((item, idx) => ( navigate(Routes.Playbacks, { projectId: item.id })} /> ))} - - {/* Colonne droite (6..N) */} - {playbacksCol2.length > 0 && ( - - {playbacksCol2.map((item, idx) => ( - - navigate(Routes.Playbacks, { projectId: item.id }) - } - /> - ))} - - )} - - + )} + ); 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 = () => ( + + {/* + + */} + + + {heroTitle} + + + {heroSubtitle} + + + + ); + + const renderWebContent = () => ( + + {renderHero()} + + + + {/* */} + + Chansons + + {/* */} + + {renderSongList()} + + + + {/* */} + + Playbacks + + {/* */} + + {renderPlaybackColumns()} + + + + ); + + const renderMobileContent = () => ( + + {renderHero()} + + {["Chansons", "Playback"].map((item, index) => ( + setSelectedCategory(item)} + tint={selectedCategory === item ? "light" : "dark"} + containerStyle={{ flex: 1 }} + titleStyle={{ + fontSize: 16, + fontFamily: FONT_FAMILY.HelveticaNeueBold, + }} + /> + ))} + + + {selectedCategory === "Chansons" && renderSongsMobile()} + {selectedCategory === "Playback" && renderPlaybacksMobile()} + {/* "Clips" pourra reprendre la même logique si tu le réactives */} + + ); + return ( @@ -255,67 +461,89 @@ const HitParade = () => { source={icons.musicLandLogo} style={{ alignSelf: "center", height: 150, resizeMode: "contain" }} /> - - - - - - - - Coups de coeur des utilisateurs catégorie {selected} - - - Les 3 {selected} les plus likées du mois reçoivent des{"\n"} - tokens. - - - + {isWeb && ( - {["Chansons", "Playback"].map((item, index) => ( - setSelected(item)} - tint={selected === item ? "light" : "dark"} - containerStyle={{ flex: 1 }} - titleStyle={{ - fontSize: 16, - fontFamily: FONT_FAMILY.HelveticaNeueBold, + + {shouldShowResults && ( + - ))} + > + + + + + + )} + )} - {selected === "Chansons" && - (isWeb ? renderSongsWeb() : renderSongsMobile())} - {selected === "Playback" && - (isWeb ? renderPlaybacksWeb() : renderPlaybacksMobile())} - {/* "Clips" pourra reprendre la même logique si tu le réactives */} + + {shouldBlurContent && ( + + )} + + {isWeb ? renderWebContent() : renderMobileContent()} diff --git a/src/screens/HitParade/components/PlaybacksCard.js b/src/screens/HitParade/components/PlaybacksCard.js index 87a1d95..b064109 100644 --- a/src/screens/HitParade/components/PlaybacksCard.js +++ b/src/screens/HitParade/components/PlaybacksCard.js @@ -20,7 +20,7 @@ const PlaybacksCard = ({ return ( )}