From bfe7fbbfd5b8db6d113719bf8379d27b993804be Mon Sep 17 00:00:00 2001 From: Jhonatan Jacome Date: Mon, 2 Dec 2024 16:38:58 -0500 Subject: [PATCH] Add Explore page #27 --- .../(page)/explore/components/SongItem.tsx | 79 +++++++++ .../(page)/explore/components/SongList.tsx | 61 +++++++ frontend/src/app/(page)/explore/layout.tsx | 20 +++ frontend/src/app/(page)/explore/page.tsx | 62 +++++++ frontend/src/components/ui/GenroButtons.tsx | 43 +++++ frontend/src/components/ui/Header.tsx | 22 ++- frontend/src/components/ui/Player.tsx | 159 ++++++++++++++++++ frontend/src/components/ui/SearchBar.tsx | 35 ++++ frontend/src/data/mockSongs.ts | 9 + frontend/src/hooks/useFilterSongs.ts | 11 ++ frontend/src/types/ui/Song.ts | 13 ++ 11 files changed, 505 insertions(+), 9 deletions(-) create mode 100644 frontend/src/app/(page)/explore/components/SongItem.tsx create mode 100644 frontend/src/app/(page)/explore/components/SongList.tsx create mode 100644 frontend/src/app/(page)/explore/layout.tsx create mode 100644 frontend/src/app/(page)/explore/page.tsx create mode 100644 frontend/src/components/ui/GenroButtons.tsx create mode 100644 frontend/src/components/ui/Player.tsx create mode 100644 frontend/src/components/ui/SearchBar.tsx create mode 100644 frontend/src/data/mockSongs.ts create mode 100644 frontend/src/hooks/useFilterSongs.ts create mode 100644 frontend/src/types/ui/Song.ts diff --git a/frontend/src/app/(page)/explore/components/SongItem.tsx b/frontend/src/app/(page)/explore/components/SongItem.tsx new file mode 100644 index 0000000..ce9f2e7 --- /dev/null +++ b/frontend/src/app/(page)/explore/components/SongItem.tsx @@ -0,0 +1,79 @@ +// SongItem.tsx +import React from "react"; +import Image from "next/image"; +import { Heart } from "lucide-react"; + +interface SongItemProps { + title: string; + artist: string; + genre: string; + duration: string; + isSelected: boolean; + isFavorite: boolean; + onFavoriteToggle: () => void; + onPlay: () => void; +} + +const SongItem: React.FC = ({ + title, + genre, + duration, + isSelected, + isFavorite, + onFavoriteToggle, + onPlay, +}) => { + return ( +
+
+ {/* Imagen de la canción */} +
+ Album art +
+ + {/* Corazón para marcar favorito */} + { + e.stopPropagation(); + onFavoriteToggle(); + }} + /> + + {/* Contenedor de la canción */} +
+ {/* Título de la canción */} +
+

{title}

+
+ + {/* Género */} +
+ {genre} +
+ + {/* Duración */} +
{duration}
+
+
+
+ ); +}; + +export default SongItem; diff --git a/frontend/src/app/(page)/explore/components/SongList.tsx b/frontend/src/app/(page)/explore/components/SongList.tsx new file mode 100644 index 0000000..406d5c3 --- /dev/null +++ b/frontend/src/app/(page)/explore/components/SongList.tsx @@ -0,0 +1,61 @@ +// SongList.tsx +import React, { useState } from "react"; +import SongItem from "./SongItem"; +import Player from "@/components/ui/Player"; +import { useFilterSongs } from "@/hooks/useFilterSongs"; +import { mockSongs } from "@/data/mockSongs"; +import { Song } from "@/types/ui/Song"; + +interface SongListProps { + searchTerm?: string; + selectedGenre?: string; +} + +export default function SongList({ + searchTerm = "", + selectedGenre = "Todos", +}: SongListProps) { + const [selectedSong, setSelectedSong] = useState(null); + const [isPlaying, setIsPlaying] = useState(false); + const [songs, setSongs] = useState(mockSongs); + + const filteredSongs = useFilterSongs(songs, searchTerm, selectedGenre); + + const handlePlay = (song: Song) => { + setSelectedSong(song); + setIsPlaying(true); + }; + + const toggleFavorite = (songId: string) => { + setSongs((prevSongs) => + prevSongs.map((song) => + song.id === songId ? { ...song, isFavorite: !song.isFavorite } : song + ) + ); + }; + + return ( +
+ {filteredSongs.map((song) => ( + handlePlay(song)} + onFavoriteToggle={() => toggleFavorite(song.id)} + /> + ))} + {selectedSong && ( + setIsPlaying(!isPlaying)} + /> + )} +
+ ); +} diff --git a/frontend/src/app/(page)/explore/layout.tsx b/frontend/src/app/(page)/explore/layout.tsx new file mode 100644 index 0000000..69461d8 --- /dev/null +++ b/frontend/src/app/(page)/explore/layout.tsx @@ -0,0 +1,20 @@ +"use client"; + +import React, { ReactNode } from "react"; + +interface ExploreLayoutProps { + children: ReactNode; +} + +const ExploreLayout: React.FC = ({ children }) => { + return ( +
+
{children}
+
+ ); +}; + +export default ExploreLayout; diff --git a/frontend/src/app/(page)/explore/page.tsx b/frontend/src/app/(page)/explore/page.tsx new file mode 100644 index 0000000..0683123 --- /dev/null +++ b/frontend/src/app/(page)/explore/page.tsx @@ -0,0 +1,62 @@ +"use client"; +import GenreButtons from "@/components/ui/GenroButtons"; +import SearchBar from "@/components/ui/SearchBar"; +import SongList from "@/app/(page)/explore/components/SongList"; +import { useState } from "react"; + +export default function ExplorePage() { + const [searchTerm, setSearchTerm] = useState(""); + const [selectedGenre, setSelectedGenre] = useState("Todos"); + + const handleSearchChange = (term: string) => { + setSearchTerm(term); + }; + + const handleGenreChange = (genre: string) => { + setSelectedGenre(genre); + }; + + return ( +
+

+ Explorar +

+
+ {/* Pantallas grandes*/} +
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+ {/* Mobile*/} +
+
+ +
+
+ +
+
+ +
+
+
+
+ ); +} diff --git a/frontend/src/components/ui/GenroButtons.tsx b/frontend/src/components/ui/GenroButtons.tsx new file mode 100644 index 0000000..3aff45d --- /dev/null +++ b/frontend/src/components/ui/GenroButtons.tsx @@ -0,0 +1,43 @@ +"use client"; + +interface GenreButtonsProps { + onGenreChange: (genre: string) => void; + selectedGenre: string; +} + +const genres = [ + "Todos", + "Electronica", + "Pop", + "Rock", + "Rap", + "K-pop", + "Clásica", +]; + +export default function GenreButtons({ + onGenreChange, + selectedGenre, +}: GenreButtonsProps) { + return ( +
+ {genres.map((genre) => ( + + ))} +
+ ); +} diff --git a/frontend/src/components/ui/Header.tsx b/frontend/src/components/ui/Header.tsx index d2432f4..387d01d 100644 --- a/frontend/src/components/ui/Header.tsx +++ b/frontend/src/components/ui/Header.tsx @@ -9,7 +9,7 @@ import { usePathname } from "next/navigation"; const navLinks: NavLink[] = [ { label: "Inicio", href: "/" }, - { label: "Explorar", href: "/explorar" }, + { label: "Explorar", href: "/explore" }, ]; const Header = () => { @@ -40,20 +40,22 @@ const Header = () => { {label} ))} @@ -96,10 +98,11 @@ const Header = () => { {label} @@ -107,10 +110,11 @@ const Header = () => { ))} diff --git a/frontend/src/components/ui/Player.tsx b/frontend/src/components/ui/Player.tsx new file mode 100644 index 0000000..03840f7 --- /dev/null +++ b/frontend/src/components/ui/Player.tsx @@ -0,0 +1,159 @@ +import React, { useEffect, useRef, useState } from "react"; +import Image from "next/image"; +import { + SkipBack, + Rewind, + Play, + Pause, + Square, + FastForward, + SkipForward, + Heart, +} from "lucide-react"; + +interface PlayerProps { + currentSong?: { + title: string; + artist: string; + duration: string; + genre: string; + }; + isPlaying: boolean; + onPlayPause: () => void; +} + +const Player: React.FC = ({ currentSong }) => { + const [isPlaying, setIsPlaying] = useState(false); + const [isFavorite, setIsFavorite] = useState(false); + const [progress, setProgress] = useState(0); // Progreso de la canción + const playerRef = useRef(null); + + useEffect(() => { + const handleScroll = () => { + const footer = document.querySelector("footer"); + if (playerRef.current && footer) { + const footerRect = footer.getBoundingClientRect(); + const windowHeight = window.innerHeight; + + if (footerRect.top <= windowHeight) { + playerRef.current.style.bottom = `${windowHeight - footerRect.top}px`; + } else { + playerRef.current.style.bottom = "0"; + } + } + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + useEffect(() => { + if (isPlaying) { + const interval = setInterval(() => { + setProgress((prev) => (prev >= 100 ? 0 : prev + 1)); + }, 1000); // Simula el progreso + return () => clearInterval(interval); + } + }, [isPlaying]); + + if (!currentSong) return null; + + return ( +
+ {/* Contenedor de la imagen */} +
+ Album art +
+ + {/* Contenedor derecho */} +
+ {/* Información de la canción */} +
+
+

{currentSong.title}

+

{currentSong.artist}

+
+ setIsFavorite(!isFavorite)} + /> +
+ {/* Controles del reproductor */} +
+ + + + + + +
+ {/* Barra de progreso */} +
+ 00:00 +
+
+
+
+ + {currentSong.duration} + +
+
+
+ ); +}; + +export default Player; diff --git a/frontend/src/components/ui/SearchBar.tsx b/frontend/src/components/ui/SearchBar.tsx new file mode 100644 index 0000000..e06312e --- /dev/null +++ b/frontend/src/components/ui/SearchBar.tsx @@ -0,0 +1,35 @@ +"use client"; +import { Search } from "lucide-react"; +import React, { useState } from "react"; + +interface SearchBarProps { + onSearchChange?: (searchTerm: string) => void; +} + +export default function SearchBar({ onSearchChange }: SearchBarProps) { + const [searchTerm, setSearchTerm] = useState(""); + + const handleSearchChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setSearchTerm(value); + onSearchChange?.(value); + }; + + return ( +
+ + +
+ ); +} diff --git a/frontend/src/data/mockSongs.ts b/frontend/src/data/mockSongs.ts new file mode 100644 index 0000000..47937ff --- /dev/null +++ b/frontend/src/data/mockSongs.ts @@ -0,0 +1,9 @@ +import { Song } from "@/types/ui/Song"; + +export const mockSongs: Song[] = [ + { id: "1", title: "Electro Waves", artist: "DJ Spark", genre: "Electronica", duration: "3:45", imageUrl: "/bg-4.jpg", isFavorite: false }, + { id: "2", title: "Pop Paradise", artist: "Melody Queen", genre: "Pop", duration: "3:20", imageUrl: "/bg-4.jpg", isFavorite: false }, + { id: "3", title: "Electro Waves", artist: "DJ Spark", genre: "Electronica", duration: "3:45", imageUrl: "/bg-4.jpg", isFavorite: false }, + { id: "4", title: "Rock Anthem", artist: "The Guitars", genre: "Rock", duration: "4:10", imageUrl: "/bg-4.jpg", isFavorite: true }, + { id: "5", title: "Anthem", artist: "The Guitars", genre: "Rock", duration: "4:10", imageUrl: "/bg-4.jpg", isFavorite: true }, +]; diff --git a/frontend/src/hooks/useFilterSongs.ts b/frontend/src/hooks/useFilterSongs.ts new file mode 100644 index 0000000..5767295 --- /dev/null +++ b/frontend/src/hooks/useFilterSongs.ts @@ -0,0 +1,11 @@ +import { Song } from "@/types/ui/Song"; + +export const useFilterSongs = (songs: Song[], searchTerm: string, selectedGenre: string): Song[] => { + return songs.filter((song) => { + const matchesSearch = [song.title, song.artist, song.genre].some((field) => + field.toLowerCase().includes(searchTerm.toLowerCase()) + ); + const matchesGenre = selectedGenre === "Todos" || song.genre === selectedGenre; + return matchesSearch && matchesGenre; + }); +}; diff --git a/frontend/src/types/ui/Song.ts b/frontend/src/types/ui/Song.ts new file mode 100644 index 0000000..36724b8 --- /dev/null +++ b/frontend/src/types/ui/Song.ts @@ -0,0 +1,13 @@ +export interface Song { + id: string; + title: string; + artist: string; + genre: string; + duration: string; + imageUrl: string; + isFavorite: boolean; +} + +export interface Genre { + name: string; +}