From 112c460efc6434a0bc8cadac27e5f540e47e9032 Mon Sep 17 00:00:00 2001 From: Jhonatan Jacome Date: Thu, 5 Dec 2024 14:49:12 -0500 Subject: [PATCH] Intercalate API and mock data, implement genre filtering, and improve image handling in Player component #27 --- frontend/next.config.mjs | 6 +- .../(page)/explore/components/SongItem.tsx | 14 +-- .../(page)/explore/components/SongList.tsx | 72 ++++++++++---- frontend/src/components/ui/Player.tsx | 42 +++++--- frontend/src/data/mockSongs.ts | 97 ++++++++++--------- frontend/src/hooks/useFilterSongs.ts | 9 +- frontend/src/services/api.ts | 14 +++ frontend/src/types/ui/Song.ts | 7 +- frontend/src/utils/fetchSongs.ts | 30 ++++++ .../src/{app => }/utils/formatDuration.ts | 0 10 files changed, 198 insertions(+), 93 deletions(-) create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/src/utils/fetchSongs.ts rename frontend/src/{app => }/utils/formatDuration.ts (100%) diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs index 4678774..eaa8022 100644 --- a/frontend/next.config.mjs +++ b/frontend/next.config.mjs @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + images: { + domains: ["cdn-images.dzcdn.net"], // Dominio para poder trabajar con las imagenes + }, +}; export default nextConfig; diff --git a/frontend/src/app/(page)/explore/components/SongItem.tsx b/frontend/src/app/(page)/explore/components/SongItem.tsx index 4d32408..0007824 100644 --- a/frontend/src/app/(page)/explore/components/SongItem.tsx +++ b/frontend/src/app/(page)/explore/components/SongItem.tsx @@ -10,7 +10,8 @@ interface SongItemProps { preview: string; md5_image: string; artist: string; - genre: string; + artistImage: string; + genres: string; isSelected: boolean; isFavorite: boolean; @@ -21,7 +22,8 @@ interface SongItemProps { const SongItem: React.FC = ({ title, - genre, + genres, + artistImage, duration, isSelected, isFavorite, @@ -41,9 +43,9 @@ const SongItem: React.FC = ({
Album art
@@ -69,12 +71,12 @@ const SongItem: React.FC = ({ {title}
- {genre} + {genres}
- {genre} + {genres}
diff --git a/frontend/src/app/(page)/explore/components/SongList.tsx b/frontend/src/app/(page)/explore/components/SongList.tsx index fedb3af..da42541 100644 --- a/frontend/src/app/(page)/explore/components/SongList.tsx +++ b/frontend/src/app/(page)/explore/components/SongList.tsx @@ -1,10 +1,11 @@ -import React, { useState, useCallback } from "react"; +import React, { useState, useEffect, useCallback } from "react"; import SongItem from "./SongItem"; import Player from "@/components/ui/Player"; +import { fetchSongs } from "@/utils/fetchSongs"; import { useFilterSongs } from "@/hooks/useFilterSongs"; -import { mockSongs } from "@/data/mockSongs"; import { Song } from "@/types/ui/Song"; -import { formatDuration } from "@/app/utils/formatDuration"; +import { formatDuration } from "@/utils/formatDuration"; +import { mockSongs } from "@/data/mockSongs"; // Importa los datos hardcodeados interface SongListProps { searchTerm?: string; @@ -17,8 +18,35 @@ export default function SongList({ }: SongListProps) { const [selectedSong, setSelectedSong] = useState(null); const [isPlaying, setIsPlaying] = useState(false); - const [songs, setSongs] = useState(mockSongs); - const filteredSongs = useFilterSongs(songs, searchTerm, selectedGenre); + const [songs, setSongs] = useState([]); // Inicializar vacío + + useEffect(() => { + const loadSongs = async () => { + try { + const fetchedSongs = await fetchSongs(searchTerm); + console.log("Fetched Songs:", fetchedSongs); // Debug log + + // Intercalar canciones de la API y mockSongs + const combinedSongs: Song[] = []; + const maxLength = Math.max(fetchedSongs.length, mockSongs.length); + for (let i = 0; i < maxLength; i++) { + if (i < fetchedSongs.length) { + combinedSongs.push(fetchedSongs[i]); + } + if (i < mockSongs.length) { + combinedSongs.push(mockSongs[i]); + } + } + + // Limitar a las primeras 15 canciones + setSongs(combinedSongs.slice(0, 15)); + } catch (error) { + console.error("Error fetching songs:", error); + } + }; + + loadSongs(); + }, [searchTerm]); const handlePlay = useCallback( (song: Song) => { @@ -38,27 +66,23 @@ export default function SongList({ const handleNext = useCallback(() => { if (!selectedSong) return; - const currentIndex = filteredSongs.findIndex( - (song) => song.id === selectedSong.id - ); - if (currentIndex < filteredSongs.length - 1) { - setSelectedSong(filteredSongs[currentIndex + 1]); + const currentIndex = songs.findIndex((song) => song.id === selectedSong.id); + if (currentIndex < songs.length - 1) { + setSelectedSong(songs[currentIndex + 1]); setIsPlaying(true); } - }, [selectedSong, filteredSongs]); + }, [selectedSong, songs]); const handlePrevious = useCallback(() => { if (!selectedSong) return; - const currentIndex = filteredSongs.findIndex( - (song) => song.id === selectedSong.id - ); + const currentIndex = songs.findIndex((song) => song.id === selectedSong.id); if (currentIndex > 0) { - setSelectedSong(filteredSongs[currentIndex - 1]); + setSelectedSong(songs[currentIndex - 1]); setIsPlaying(true); } - }, [selectedSong, filteredSongs]); + }, [selectedSong, songs]); - const toggleFavorite = useCallback((songId: string) => { + const toggleFavoriteHandler = useCallback((songId: string) => { setSongs((prevSongs) => prevSongs.map((song) => song.id === songId ? { ...song, isFavorite: !song.isFavorite } : song @@ -66,6 +90,9 @@ export default function SongList({ ); }, []); + const filteredSongs = useFilterSongs(songs, searchTerm, selectedGenre); + console.log("Filtered Songs:", filteredSongs); // Debug log + return (
@@ -75,15 +102,16 @@ export default function SongList({ id={song.id} title={song.title} title_short={song.title_short} - genre={song.genre} + genres={song.album.genres || ""} duration={formatDuration(song.duration)} md5_image={song.md5_image} preview={song.preview} artist={song.artist.name} + artistImage={song.artist.picture_medium} isFavorite={song.isFavorite} isSelected={selectedSong?.id === song.id} onPlay={() => handlePlay(song)} - onFavoriteToggle={() => toggleFavorite(song.id)} + onFavoriteToggle={() => toggleFavoriteHandler(song.id)} /> ))}
@@ -91,15 +119,17 @@ export default function SongList({ - selectedSong && toggleFavorite(selectedSong.id) + selectedSong && toggleFavoriteHandler(selectedSong.id) } /> )} diff --git a/frontend/src/components/ui/Player.tsx b/frontend/src/components/ui/Player.tsx index 84ebd68..30fd3a5 100644 --- a/frontend/src/components/ui/Player.tsx +++ b/frontend/src/components/ui/Player.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from "react"; import Image from "next/image"; -import { formatDuration } from "@/app/utils/formatDuration"; +import { formatDuration } from "@/utils/formatDuration"; import { SkipBack, Rewind, @@ -18,10 +18,11 @@ interface PlayerProps { title: string; artist: string; duration: string; - genre: string; preview: string; isFavorite: boolean; }; + genres: string; + artistImage: string; isPlaying: boolean; onPlayPause: () => void; onNext?: () => void; @@ -32,6 +33,8 @@ interface PlayerProps { const Player: React.FC = ({ currentSong, + genres, + artistImage, isPlaying, onPlayPause, onNext, @@ -90,14 +93,6 @@ const Player: React.FC = ({ } }, [isPlaying, currentSong]); - const updateProgress = () => { - if (!audioRef.current) return; - const duration = audioRef.current.duration; - const currentTime = audioRef.current.currentTime; - const progressPercent = (currentTime / duration) * 100; - setProgress(progressPercent); - setCurrentTime(formatTime(currentTime)); - }; //Para que el player se mueva con el scroll y no pase a cubrir el footer useEffect(() => { const handleScroll = () => { @@ -118,6 +113,15 @@ const Player: React.FC = ({ return () => window.removeEventListener("scroll", handleScroll); }, []); + const updateProgress = () => { + if (!audioRef.current) return; + const duration = audioRef.current.duration; + const currentTime = audioRef.current.currentTime; + const progressPercent = (currentTime / duration) * 100; + setProgress(progressPercent); + setCurrentTime(formatTime(currentTime)); + }; + const handleSongEnd = () => { setProgress(0); setCurrentTime("00:00"); @@ -161,6 +165,15 @@ const Player: React.FC = ({ ); }; + const handleStop = () => { + if (audioRef.current) { + audioRef.current.pause(); + audioRef.current.currentTime = 0; + setProgress(0); + setCurrentTime("00:00"); + } + }; + const formatTime = (time: number): string => { const minutes = Math.floor(time / 60); const seconds = Math.floor(time % 60); @@ -178,10 +191,10 @@ const Player: React.FC = ({ >
Album art
@@ -191,6 +204,7 @@ const Player: React.FC = ({

{currentSong.title}

{currentSong.artist}

+

{genres}

= ({ className="text-primary cursor-pointer" size={47} style={{ strokeWidth: 2, fill: "currentColor" }} - onClick={onPlayPause} + onClick={handleStop} // Cambia a handleStop /> { + if (!Array.isArray(songs)) { + return []; + } + return songs.filter((song) => { - const matchesSearch = [song.title, song.artist, song.genre] + const matchesSearch = [song.title, song.artist.name, song.album.genres] .map((field) => (typeof field === "string" ? field : "").toLowerCase()) .some((value) => value.includes(searchTerm.toLowerCase())); + const genres = song.album.genres || ""; const matchesGenre = - selectedGenre === "Todos" || song.genre === selectedGenre; + selectedGenre === "Todos" || genres.toLowerCase().includes(selectedGenre.toLowerCase()); return matchesSearch && matchesGenre; }); diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..886276d --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,14 @@ +export const fetchSongs = async (track: string) => { + const response = await fetch(`http://144.33.15.219:8080/search?track=${encodeURIComponent(track)}`, { + headers: { + 'accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Error fetching songs'); + } + + const data = await response.json(); + return data; +}; diff --git a/frontend/src/types/ui/Song.ts b/frontend/src/types/ui/Song.ts index 371e338..14955c3 100644 --- a/frontend/src/types/ui/Song.ts +++ b/frontend/src/types/ui/Song.ts @@ -9,13 +9,9 @@ export interface Song { artist: Artist; album: Album; isFavorite: boolean; - genre: string; - imageUrl: string; } -export interface Genre { - name: string; -} + export interface Artist { id: string; @@ -38,4 +34,5 @@ export interface Album { cover_xl: string; md5_image: string; tracklist: string; + genres: string | null; } diff --git a/frontend/src/utils/fetchSongs.ts b/frontend/src/utils/fetchSongs.ts new file mode 100644 index 0000000..30240e0 --- /dev/null +++ b/frontend/src/utils/fetchSongs.ts @@ -0,0 +1,30 @@ +export const fetchSongs = async (track: string) => { + const genres = ["pop", "rock", "electronica", "clasica", "hip-hop", "rap", "k-pop"]; + + const formattedTrack = track ? `'${track}'` : `'${genres[Math.floor(Math.random() * genres.length)]}'`; + //console.log(`Fetching songs for track: ${formattedTrack}`); // Debug log + + const response = await fetch(`http://144.33.15.219:8080/search?track=${encodeURIComponent(formattedTrack)}`, { + headers: { + 'accept': 'application/json' + } + }); + + if (!response.ok) { + throw new Error('Error fetching songs'); + } + + const data = await response.json(); + //console.log('API Response:', data); // Debug log + + // Asegurarse de acceder al arreglo de canciones dentro de la estructura de datos + const songs = data.data.data || []; + //console.log('Extracted Songs:', songs); // Debug log + + if (songs.length > 0) { + return songs; + } else { + console.warn('No songs found'); + return []; + } +}; diff --git a/frontend/src/app/utils/formatDuration.ts b/frontend/src/utils/formatDuration.ts similarity index 100% rename from frontend/src/app/utils/formatDuration.ts rename to frontend/src/utils/formatDuration.ts