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
Configuración del proyecto
Estructura de carpetas
Creando el layout principal
Implementando el Banner
Sección Mood Weekend con scroll horizontal
Sección Top Albums
Sección Browse All con Flex Wrap
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:
Layout principal con flexbox para organizar los componentes
Banner promocional con gradiente y botón de acción
Sección Mood Weekend con scroll horizontal usando flex-direction: row
Sección Top Albums también con scroll horizontal
Sección Browse All usando flex-wrap para que los elementos fluyan a la siguiente línea
Header con barra de búsqueda y usuario
Reproductor fijo en la parte inferior
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
Publicar un comentario