27. Resultado de Aplicación - Music App

 

Tutorial: Creando una Music App en React con Layouts Flexbox

Introducción

¡Bienvenidos al tutorial de creación de una aplicación de música en React! En esta sección estaremos utilizando mucho el uso de los layouts y utilizando también lo que es Flex Wrap y Flex Direction Row.

En este caso tendremos también en la aplicación lo que es un banner. Abajo de este banner se ubica nuestra primer contenido en este caso es Mood Weekend. Como vemos, tiene un space between dentro del texto y un icono. Ese icono lo que nos indica es que tenemos un scroll de forma horizontal, pudiendo ver nuestro contenido de forma horizontal de Mood Weekend.

También tendremos nuestra sección de Top Albums en las cuales mostraremos los albums que están en el top en estos momentos y adicionalmente a ello, tendremos lo que es la sección de Browse All, que tendrá lo que es un Flex Wrap en el cual el contenido se continúa en la siguiente sección y ya no se va de forma horizontal como en este caso.

Esto y más estaremos viendo en esta sección. Espero que les guste.

Tabla de Contenidos

  1. Configuración del proyecto

  2. Estructura de carpetas

  3. Creando el layout principal

  4. Implementando el Banner

  5. Sección Mood Weekend con scroll horizontal

  6. Sección Top Albums

  7. Sección Browse All con Flex Wrap

  8. Estilos responsivos


1. Configuración del proyecto

Primero, crearemos nuestra aplicación React:

bash

npx create-react-app music-app

cd music-app

npm install

Para los iconos, instalaremos React Icons:

bash

npm install react-icons

2. Estructura de carpetas

Vamos a organizar nuestro proyecto de la siguiente manera:

text

music-app/

├── src/

│   ├── components/

│   │   ├── Banner.js

│   │   ├── MoodWeekend.js

│   │   ├── TopAlbums.js

│   │   ├── BrowseAll.js

│   │   ├── Header.js

│   │   └── Player.js

│   ├── styles/

│   │   ├── App.css

│   │   └── components.css

│   ├── assets/

│   │   └── images/

│   ├── App.js

│   └── index.js

3. Creando el layout principal

App.js

jsx

import React from 'react';

import './styles/App.css';

import Header from './components/Header';

import Banner from './components/Banner';

import MoodWeekend from './components/MoodWeekend';

import TopAlbums from './components/TopAlbums';

import BrowseAll from './components/BrowseAll';

import Player from './components/Player';


function App() {

  return (

    <div className="app">

      <Header />

      <main className="main-content">

        <Banner />

        <MoodWeekend />

        <TopAlbums />

        <BrowseAll />

      </main>

      <Player />

    </div>

  );

}


export default App;

styles/App.css

css

* {

  margin: 0;

  padding: 0;

  box-sizing: border-box;

  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;

}


.app {

  display: flex;

  flex-direction: column;

  min-height: 100vh;

  background-color: #121212;

  color: #ffffff;

}


.main-content {

  flex: 1;

  padding: 0 20px;

  overflow-y: auto;

  padding-bottom: 100px;

}


/* Estilos para scrollbar */

.main-content::-webkit-scrollbar {

  width: 8px;

}


.main-content::-webkit-scrollbar-track {

  background: #1e1e1e;

}


.main-content::-webkit-scrollbar-thumb {

  background: #555;

  border-radius: 4px;

}


.main-content::-webkit-scrollbar-thumb:hover {

  background: #888;

}


/* Clases utilitarias para flexbox */

.flex-between {

  display: flex;

  justify-content: space-between;

  align-items: center;

}


.flex-row {

  display: flex;

  flex-direction: row;

}


.flex-wrap {

  display: flex;

  flex-wrap: wrap;

}


.section-title {

  font-size: 24px;

  font-weight: 700;

  margin: 20px 0;

}


.section-container {

  margin-bottom: 40px;

}

4. Implementando el Banner

components/Banner.js

jsx

import React from 'react';

import '../styles/components.css';


const Banner = () => {

  return (

    <div className="banner">

      <div className="banner-content">

        <h1 className="banner-title">Descubre nueva música</h1>

        <p className="banner-subtitle">Explora los mejores álbumes y playlists del momento</p>

        <button className="banner-button">Comenzar ahora</button>

      </div>

    </div>

  );

};


export default Banner;

styles/components.css (primera parte)

css

/* Banner styles */

.banner {

  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

  border-radius: 20px;

  padding: 40px;

  margin: 20px 0;

  height: 250px;

  display: flex;

  align-items: center;

}


.banner-content {

  max-width: 60%;

}


.banner-title {

  font-size: 2.5rem;

  font-weight: 800;

  margin-bottom: 10px;

}


.banner-subtitle {

  font-size: 1.2rem;

  color: rgba(255, 255, 255, 0.9);

  margin-bottom: 20px;

}


.banner-button {

  background-color: #1DB954;

  color: white;

  border: none;

  padding: 12px 30px;

  border-radius: 30px;

  font-size: 1rem;

  font-weight: 600;

  cursor: pointer;

  transition: all 0.3s ease;

}


.banner-button:hover {

  background-color: #1ed760;

  transform: scale(1.05);

}

5. Sección Mood Weekend con scroll horizontal

components/MoodWeekend.js

jsx

import React from 'react';

import { FiChevronRight } from 'react-icons/fi';

import '../styles/components.css';


const MoodWeekend = () => {

  const moods = [

    { id: 1, name: 'Relax', color: '#667eea', emoji: '😌' },

    { id: 2, name: 'Energético', color: '#f093fb', emoji: '⚡' },

    { id: 3, name: 'Nostálgico', color: '#f5576c', emoji: '📻' },

    { id: 4, name: 'Feliz', color: '#4facfe', emoji: '😄' },

    { id: 5, name: 'Triste', color: '#764ba2', emoji: '😢' },

    { id: 6, name: 'Concentración', color: '#43e97b', emoji: '🎯' },

    { id: 7, name: 'Fiesta', color: '#fa709a', emoji: '🎉' },

    { id: 8, name: 'Romántico', color: '#ff6b6b', emoji: '❤️' },

  ];


  return (

    <div className="section-container">

      <div className="section-header flex-between">

        <h2 className="section-title">Mood Weekend</h2>

        <div className="section-header-icon">

          <FiChevronRight size={24} />

          <span>Ver todo</span>

        </div>

      </div>

      

      <div className="horizontal-scroll-container">

        <div className="horizontal-scroll-content">

          {moods.map((mood) => (

            <div 

              key={mood.id} 

              className="mood-card"

              style={{ backgroundColor: mood.color }}

            >

              <div className="mood-emoji">{mood.emoji}</div>

              <h3 className="mood-name">{mood.name}</h3>

            </div>

          ))}

        </div>

      </div>

    </div>

  );

};


export default MoodWeekend;

styles/components.css (continuación)

css

/* Section Header */

.section-header {

  margin-bottom: 20px;

}


.section-header-icon {

  display: flex;

  align-items: center;

  gap: 8px;

  color: #b3b3b3;

  cursor: pointer;

  transition: color 0.3s;

}


.section-header-icon:hover {

  color: #ffffff;

}


/* Mood Weekend - Horizontal Scroll */

.horizontal-scroll-container {

  overflow-x: auto;

  padding-bottom: 10px;

}


.horizontal-scroll-container::-webkit-scrollbar {

  height: 6px;

}


.horizontal-scroll-container::-webkit-scrollbar-track {

  background: #1e1e1e;

  border-radius: 3px;

}


.horizontal-scroll-container::-webkit-scrollbar-thumb {

  background: #444;

  border-radius: 3px;

}


.horizontal-scroll-content {

  display: flex;

  flex-direction: row;

  gap: 20px;

  padding: 5px 0;

}


.mood-card {

  min-width: 160px;

  height: 180px;

  border-radius: 15px;

  padding: 20px;

  display: flex;

  flex-direction: column;

  justify-content: flex-end;

  cursor: pointer;

  transition: transform 0.3s ease;

}


.mood-card:hover {

  transform: translateY(-5px);

}


.mood-emoji {

  font-size: 40px;

  margin-bottom: 10px;

}


.mood-name {

  font-size: 18px;

  font-weight: 600;

}

6. Sección Top Albums

components/TopAlbums.js

jsx

import React from 'react';

import { FiMoreHorizontal, FiPlay } from 'react-icons/fi';

import '../styles/components.css';


const TopAlbums = () => {

  const albums = [

    { id: 1, title: 'Midnight Dreams', artist: 'The Dreamers', plays: '2.5M', color: '#FF6B6B' },

    { id: 2, title: 'Summer Vibes', artist: 'Ocean View', plays: '1.8M', color: '#4ECDC4' },

    { id: 3, title: 'Urban Legends', artist: 'City Lights', plays: '3.2M', color: '#45B7D1' },

    { id: 4, title: 'Echoes of Time', artist: 'Nova Sequence', plays: '1.2M', color: '#96CEB4' },

    { id: 5, title: 'Neon Nights', artist: 'Synthwave Collective', plays: '2.1M', color: '#FFEAA7' },

  ];


  return (

    <div className="section-container">

      <div className="section-header flex-between">

        <h2 className="section-title">Top Albums</h2>

        <div className="section-header-icon">

          <FiMoreHorizontal size={24} />

          <span>Ver más</span>

        </div>

      </div>

      

      <div className="horizontal-scroll-container">

        <div className="horizontal-scroll-content">

          {albums.map((album) => (

            <div key={album.id} className="album-card">

              <div 

                className="album-cover"

                style={{ backgroundColor: album.color }}

              >

                <div className="album-play-overlay">

                  <FiPlay size={30} />

                </div>

              </div>

              <div className="album-info">

                <h3 className="album-title">{album.title}</h3>

                <p className="album-artist">{album.artist}</p>

                <div className="album-plays">

                  <span className="plays-count">{album.plays}</span>

                  <span className="plays-text"> reproducciones</span>

                </div>

              </div>

            </div>

          ))}

        </div>

      </div>

    </div>

  );

};


export default TopAlbums;

styles/components.css (continuación)

css

/* Top Albums */

.album-card {

  min-width: 200px;

  background-color: #1e1e1e;

  border-radius: 10px;

  padding: 15px;

  cursor: pointer;

  transition: background-color 0.3s;

}


.album-card:hover {

  background-color: #282828;

}


.album-cover {

  width: 100%;

  height: 170px;

  border-radius: 8px;

  margin-bottom: 15px;

  position: relative;

  overflow: hidden;

}


.album-play-overlay {

  position: absolute;

  top: 0;

  left: 0;

  width: 100%;

  height: 100%;

  background-color: rgba(0, 0, 0, 0.5);

  display: flex;

  justify-content: center;

  align-items: center;

  opacity: 0;

  transition: opacity 0.3s;

}


.album-card:hover .album-play-overlay {

  opacity: 1;

}


.album-title {

  font-size: 16px;

  font-weight: 600;

  margin-bottom: 5px;

  white-space: nowrap;

  overflow: hidden;

  text-overflow: ellipsis;

}


.album-artist {

  font-size: 14px;

  color: #b3b3b3;

  margin-bottom: 10px;

}


.album-plays {

  display: flex;

  align-items: center;

  font-size: 14px;

}


.plays-count {

  font-weight: 600;

  color: #1DB954;

}


.plays-text {

  color: #b3b3b3;

  margin-left: 5px;

}

7. Sección Browse All con Flex Wrap

components/BrowseAll.js

jsx

import React from 'react';

import '../styles/components.css';


const BrowseAll = () => {

  const categories = [

    { id: 1, name: 'Pop', color: '#8A2387' },

    { id: 2, name: 'Rock', color: '#F27121' },

    { id: 3, name: 'Hip Hop', color: '#E94057' },

    { id: 4, name: 'Jazz', color: '#4A00E0' },

    { id: 5, name: 'Electrónica', color: '#24C6DC' },

    { id: 6, name: 'Indie', color: '#514A9D' },

    { id: 7, name: 'Reggaetón', color: '#FF8008' },

    { id: 8, name: 'Clásica', color: '#1D976C' },

    { id: 9, name: 'R&B', color: '#DA4453' },

    { id: 10, name: 'Metal', color: '#3A1C71' },

    { id: 11, name: 'Country', color: '#D66D75' },

    { id: 12, name: 'Alternativo', color: '#2193B0' },

  ];


  return (

    <div className="section-container">

      <div className="section-header flex-between">

        <h2 className="section-title">Browse All</h2>

      </div>

      

      <div className="browse-all-container">

        {categories.map((category) => (

          <div 

            key={category.id} 

            className="category-card"

            style={{ backgroundColor: category.color }}

          >

            <h3 className="category-name">{category.name}</h3>

          </div>

        ))}

      </div>

    </div>

  );

};


export default BrowseAll;

styles/components.css (continuación)

css

/* Browse All - Flex Wrap */

.browse-all-container {

  display: flex;

  flex-wrap: wrap;

  gap: 20px;

}


.category-card {

  flex: 1 0 calc(25% - 20px);

  min-width: 200px;

  height: 150px;

  border-radius: 10px;

  padding: 20px;

  display: flex;

  align-items: flex-end;

  cursor: pointer;

  transition: transform 0.3s ease, filter 0.3s ease;

}


.category-card:hover {

  transform: scale(1.03);

  filter: brightness(1.1);

}


.category-name {

  font-size: 24px;

  font-weight: 700;

}

8. Componentes adicionales

components/Header.js

jsx

import React from 'react';

import { FiSearch, FiBell } from 'react-icons/fi';

import '../styles/components.css';


const Header = () => {

  return (

    <header className="header">

      <div className="logo">

        <h1>Music<span>App</span></h1>

      </div>

      

      <div className="header-actions flex-between">

        <div className="search-bar">

          <FiSearch className="search-icon" />

          <input type="text" placeholder="Buscar artistas, canciones o álbumes..." />

        </div>

        

        <div className="user-actions">

          <button className="icon-button">

            <FiBell size={20} />

          </button>

          <div className="user-avatar">

            <img src="https://i.pravatar.cc/40" alt="Usuario" />

          </div>

        </div>

      </div>

    </header>

  );

};


export default Header;

components/Player.js

jsx

import React from 'react';

import { FiPlay, FiPause, FiSkipBack, FiSkipForward, FiVolume2 } from 'react-icons/fi';

import '../styles/components.css';


const Player = () => {

  const [isPlaying, setIsPlaying] = React.useState(false);


  return (

    <div className="player">

      <div className="now-playing">

        <div className="now-playing-cover"></div>

        <div className="now-playing-info">

          <h4>Canción Ejemplo</h4>

          <p>Artista Ejemplo</p>

        </div>

      </div>

      

      <div className="player-controls">

        <div className="control-buttons">

          <button className="control-button">

            <FiSkipBack size={20} />

          </button>

          <button 

            className="control-button play-button"

            onClick={() => setIsPlaying(!isPlaying)}

          >

            {isPlaying ? <FiPause size={24} /> : <FiPlay size={24} />}

          </button>

          <button className="control-button">

            <FiSkipForward size={20} />

          </button>

        </div>

        

        <div className="progress-bar">

          <span className="time-current">1:23</span>

          <div className="progress-track">

            <div className="progress-fill" style={{ width: '30%' }}></div>

          </div>

          <span className="time-total">3:45</span>

        </div>

      </div>

      

      <div className="player-volume">

        <FiVolume2 size={20} />

        <div className="volume-slider">

          <div className="volume-fill" style={{ width: '70%' }}></div>

        </div>

      </div>

    </div>

  );

};


export default Player;

styles/components.css (final)

css

/* Header */

.header {

  display: flex;

  justify-content: space-between;

  align-items: center;

  padding: 20px;

  background-color: #000000;

  position: sticky;

  top: 0;

  z-index: 100;

}


.logo h1 {

  font-size: 28px;

  font-weight: 800;

}


.logo span {

  color: #1DB954;

}


.search-bar {

  display: flex;

  align-items: center;

  background-color: #282828;

  border-radius: 30px;

  padding: 10px 20px;

  width: 500px;

}


.search-icon {

  color: #b3b3b3;

  margin-right: 10px;

}


.search-bar input {

  background: none;

  border: none;

  color: white;

  width: 100%;

  font-size: 14px;

}


.search-bar input:focus {

  outline: none;

}


.user-actions {

  display: flex;

  align-items: center;

  gap: 20px;

}


.icon-button {

  background: none;

  border: none;

  color: white;

  cursor: pointer;

  padding: 8px;

  border-radius: 50%;

  transition: background-color 0.3s;

}


.icon-button:hover {

  background-color: #282828;

}


.user-avatar img {

  width: 40px;

  height: 40px;

  border-radius: 50%;

  object-fit: cover;

}


/* Player */

.player {

  position: fixed;

  bottom: 0;

  left: 0;

  right: 0;

  background-color: #181818;

  border-top: 1px solid #282828;

  padding: 15px 20px;

  display: flex;

  justify-content: space-between;

  align-items: center;

  z-index: 1000;

}


.now-playing {

  display: flex;

  align-items: center;

  gap: 15px;

  flex: 1;

}


.now-playing-cover {

  width: 60px;

  height: 60px;

  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);

  border-radius: 5px;

}


.now-playing-info h4 {

  font-size: 14px;

  margin-bottom: 5px;

}


.now-playing-info p {

  font-size: 12px;

  color: #b3b3b3;

}


.player-controls {

  flex: 2;

  display: flex;

  flex-direction: column;

  align-items: center;

  gap: 10px;

}


.control-buttons {

  display: flex;

  align-items: center;

  gap: 20px;

}


.control-button {

  background: none;

  border: none;

  color: white;

  cursor: pointer;

  padding: 8px;

  border-radius: 50%;

  transition: background-color 0.3s;

}


.control-button:hover {

  background-color: #282828;

}


.play-button {

  background-color: white;

  color: black;

  width: 40px;

  height: 40px;

  display: flex;

  justify-content: center;

  align-items: center;

}


.play-button:hover {

  background-color: #f0f0f0;

}


.progress-bar {

  display: flex;

  align-items: center;

  gap: 15px;

  width: 100%;

  max-width: 600px;

}


.time-current, .time-total {

  font-size: 12px;

  color: #b3b3b3;

  min-width: 40px;

}


.progress-track {

  flex: 1;

  height: 4px;

  background-color: #404040;

  border-radius: 2px;

  cursor: pointer;

}


.progress-fill {

  height: 100%;

  background-color: #1DB954;

  border-radius: 2px;

  position: relative;

}


.progress-fill::after {

  content: '';

  position: absolute;

  right: 0;

  top: 50%;

  transform: translateY(-50%);

  width: 12px;

  height: 12px;

  background-color: white;

  border-radius: 50%;

  opacity: 0;

  transition: opacity 0.3s;

}


.progress-track:hover .progress-fill::after {

  opacity: 1;

}


.player-volume {

  flex: 1;

  display: flex;

  justify-content: flex-end;

  align-items: center;

  gap: 10px;

}


.volume-slider {

  width: 100px;

  height: 4px;

  background-color: #404040;

  border-radius: 2px;

  cursor: pointer;

}


.volume-fill {

  height: 100%;

  background-color: #1DB954;

  border-radius: 2px;

}


/* Responsive Design */

@media (max-width: 1200px) {

  .category-card {

    flex: 1 0 calc(33.333% - 20px);

  }

  

  .search-bar {

    width: 300px;

  }

}


@media (max-width: 768px) {

  .main-content {

    padding: 0 10px;

  }

  

  .banner {

    height: 200px;

    padding: 20px;

  }

  

  .banner-title {

    font-size: 2rem;

  }

  

  .banner-subtitle {

    font-size: 1rem;

  }

  

  .category-card {

    flex: 1 0 calc(50% - 20px);

  }

  

  .search-bar {

    width: 200px;

  }

  

  .header {

    flex-direction: column;

    gap: 15px;

    padding: 15px;

  }

  

  .player {

    flex-direction: column;

    gap: 15px;

    padding: 10px;

  }

}


@media (max-width: 480px) {

  .category-card {

    flex: 1 0 100%;

  }

  

  .mood-card {

    min-width: 140px;

    height: 160px;

  }

  

  .album-card {

    min-width: 170px;

  }

  

  .search-bar {

    width: 100%;

  }

}

9. Ejecutar la aplicación

Finalmente, ejecuta la aplicación:

bash

npm start

Tu aplicación estará disponible en http://localhost:3000

Resumen del tutorial

Hemos creado una aplicación de música en React que incluye:

  1. Layout principal con flexbox para organizar los componentes

  2. Banner promocional con gradiente y botón de acción

  3. Sección Mood Weekend con scroll horizontal usando flex-direction: row

  4. Sección Top Albums también con scroll horizontal

  5. Sección Browse All usando flex-wrap para que los elementos fluyan a la siguiente línea

  6. Header con barra de búsqueda y usuario

  7. Reproductor fijo en la parte inferior

  8. Diseño responsivo que se adapta a diferentes tamaños de pantalla

Este proyecto demuestra el uso práctico de flexbox para crear layouts modernos y responsivos. Puedes expandirlo agregando funcionalidades como reproducción real de música, búsqueda, y integración con APIs de música


Comentarios

Entradas más populares de este blog

18. Visualizar nuestros componentes limpios con StyleSheet

15. Componente Image

Tema: 23. Justify Content en Flexbox para Column (Columna) en React Nativ