Aula 3: Front-end com React (parte 2) – SpotCode | OneBitCode
Códigos completos: https://github.com/OneBitCodeBlog/SpotcodeV2
• Conectando a página discovery com o Backend – 00:00:57
• Criando a página Álbum – 00:14:29
• Conectando o Álbum com o Backend – 00:19:54
• Player de música (template básico) – 00:26:11
• Player de música (exibindo as músicas reais) – 00:34:37
• Player de música (Tocando as músicas) – 00:43:40
• Marcando uma música como ouvida – 00:55:34
• Incluindo o Favorites – 01:00:21
• Criando a página de Search – 01:09:46
• Criando a busca por texto e categoria – 01:13:09
• Criando os components Categories e SearchBar – 01:16:56
• Criando a Tab de resultados – 01:27:12
• Criando a página de favoritos – 01:39:10
• Transformando o APP em um PWA – 01:43:31
• Desafios – 01:57:21
Conectando a página discovery com o Backend
0 – Atualize o nome do service discovery para albums
1 – No discovery inclua o useState, o useEffect e importe o service que criamos
1 2 3 |
import React, { Fragment, useEffect, useState } from 'react'; import AlbumsService from '../../services/albums' |
2 – Inclua os states no discovery
1 2 |
const [recent_albums, setRecentAlbums] = useState([]); const [recommended_albums, setRecommendedAlbums] = useState([]); |
3 – Crie um método para baixar as informações do backend através do service e depois salvar nos states
1 2 3 4 5 |
async function fetchAlbums() { const response = await AlbumsService.index(); setRecentAlbums(response.data['recent_albums']) setRecommendedAlbums(response.data['recommended_albums']) } |
4 – Chame ele sempre que o component iniciar
1 2 3 |
useEffect(() => { fetchAlbums(); }, []); |
5 – Remova os dados mockados e coloque no lugar
1 2 3 4 5 6 7 8 9 10 11 |
const recent_albums_components = recent_albums.map((album, key) => <Columns.Column desktop={{ size: 3 }} mobile={{ size: 6 }} key={album}> <Album artist_name={album.artist_name} title={album.title} cover_url={album.cover_url} key={key} id={album.id}/> </Columns.Column> ); const recommended_albums_components = recommended_albums.map((album, key) => <Columns.Column desktop={{ size: 3 }} mobile={{ size: 6 }} key={key}> <Album artist_name={album.artist_name} title={album.title} cover_url={album.cover_url} key={key} id={album.id}/> </Columns.Column> ); |
6 – Atualize a estrutura (condicional para mostrar albuns apenas quando eles forem retornados da API)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
{recent_albums_components.length > 0 && <div> <Heading className='has-text-white' size={4}> Tocadas recentemente </Heading> <Columns className='is-mobile'> {recent_albums_components} </Columns> </div> } {recommended_albums_components.length > 0 && <DivVSpaced> <Heading className='has-text-white' size={4}> Recomendadas </Heading> <Columns className='is-mobile'> {recommended_albums_components} </Columns> </DivVSpaced> } |
Criando a página Álbum
1 – Mova o component album para /common
2 – Em discovery atualize o importe de Álbum colocando:
1 |
import Album from '../common/album'; |
3 – Crie em um component chamado albums e dentro dele um arquivo chamado index.js e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 |
import React, { Fragment, useState, useEffect } from 'react'; const Albums = () => { return ( <Fragment> </Fragment> ); } export default Albums; |
4 – Na Screen Album importe o novo component deixando assim:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import React, { Fragment } from 'react'; import NavbarFooter from '../../components/common/navbar_footer'; import Albums from '../../components/albums'; import SectionWrapper from '../../components/common/section_wrapper' const AlbumScreen = () => { return( <Fragment> <SectionWrapper> <Albums/> <NavbarFooter/> </SectionWrapper> </Fragment> ); } export default AlbumScreen; |
5 – Vamos deixar a estrutura do component album preparado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
import React, { Fragment, useState, useEffect } from 'react'; import { Heading, Columns, Image } from 'react-bulma-components'; import styled from 'styled-components'; const DivVSpaced = styled.div` margin-top: 20px; margin-bottom: 20px; ` const Albums = () => { return ( <Fragment> <Columns className='is-vcentered is-mobile is-centered'> <Columns.Column desktop={{size: 3}} className='has-text-centered'> <Image src=''/> <DivVSpaced> <Heading size={5} className='has-text-white'>Título</Heading> <Heading size={6} subtitle className='has-text-white'>SubTítulo</Heading> </DivVSpaced> </Columns.Column> </Columns> </Fragment> ); } export default Albums; |
Conectando o Album com o Backend
1 – No service Albums inclua:
1 2 3 |
... show: (id) => Api.get(`/albums/${id}`) ... |
2 – Importe o service no component Album:
1 |
import AlbumsService from '../../services/albums'; |
3 – Para pegar o :id do album na URL inclua:
1 2 3 4 5 6 7 |
... import { useParams } from 'react-router-dom' ... const Albums = () => { let { id } = useParams(); ... |
4 – Para chamar a API quando montar o component e armazenar o album como um estado inclua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import React, { Fragment, useState, useEffect } from 'react'; ... const Albums = () => { const [album, setAlbum] = useState({}); let { id } = useParams(); async function fetchAlbum() { const response = await AlbumsService.show(id); setAlbum(response.data) } useEffect(() => { fetchAlbum(); }, []); ... |
5 – Agora atualize o template pra que as informações reais sejam exibidas:
1 2 3 4 5 6 7 8 9 10 11 |
<Fragment> <Columns className='is-vcentered is-mobile is-centered'> <Columns.Column desktop={{size: 3}} className='has-text-centered'> <Image src={album.cover_url}/> <DivVSpaced> <Heading size={5} className='has-text-white'>{album.title}</Heading> <Heading size={6} subtitle className='has-text-white'>{album.artist_name}</Heading> </DivVSpaced> </Columns.Column> </Columns> </Fragment> |
6 – No arquivo /views/albums/show.json.jbuilder inclua:
1 |
json.artist_name @album.artist.name |
Player de música (template básico)
1 – Crie um component chamado Musics e coloque nele:
1 2 3 4 5 6 7 8 9 10 |
import React, { Fragment } from 'react'; const Musics = (props) => { return ( <Fragment> </Fragment> ); } export default Musics; |
2 – Dentro da pasta do component Musics crie um component chamado Music e coloque nele:
1 2 3 4 5 6 7 8 9 10 |
import React, { Fragment } from 'react'; const Music = (props) => { return ( <Fragment> </Fragment> ); } export default Music; |
3 – No component Album importe o novo component e chame ele no layout:
1 2 3 4 5 6 7 8 |
... import Musics from '../musics'; ... <Fragment> ... <Musics songs={album.songs || []}/> </Fragment> ... |
4 – No component Musics vamos incluir o layout básico:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
... import styled from 'styled-components'; const PlaySequenceButton = styled(Button)` margin-bottom: 30px; ` ... <Fragment> <Columns className='is-mobile is-centered'> <Columns.Column desktop={{size: 2}} mobile={{size: 12}} className='has-text-centered'> <PlaySequenceButton className='is-medium' color='primary' outlined> Tocar em sequência </PlaySequenceButton> </Columns.Column> </Columns> {/* Musics :) */} </Fragment> ... |
5 – Importe o component Music em Musics:
1 2 3 |
... import Music from './music'; ... |
6 – Agora vamos desenvolver o template básico de Music:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
... import { Heading, Columns } from 'react-bulma-components'; import styled from 'styled-components' import { FaPlayCircle, FaStopCircle } from "react-icons/fa"; const MusicSeparator = styled.hr` height: 1px; margin-top: 0px; ` const CustomSubHeading = styled(Heading)` margin-bottom: 0px !important; ` ... <Fragment> <Columns className='is-vcentered is-mobile is-centered'> <Columns.Column desktop={{ size: 1 }} mobile={{ size: 2 }} > <FaPlayCircle size='45px' className='has-text-white' /> </Columns.Column> <Columns.Column desktop={{ size: 4 }} mobile={{ size: 8 }}> <Columns className='is-vcentered is-mobile'> <Columns.Column desktop={{ size: 8 }} mobile={{ size: 8 }}> <Heading size={5} className='has-text-white'> Title </Heading> <CustomSubHeading size={6} className='has-text-white' subtitle> Artist Name </CustomSubHeading> </Columns.Column> <Columns.Column desktop={{ size: 4 }} mobile={{ size: 4 }} className='is-pulled-right has-text-right'> {/* Favorite */} </Columns.Column> </Columns> <MusicSeparator /> </Columns.Column> </Columns> </Fragment> ... |
7 – Exiba o Músic no Musics:
1 2 3 4 5 6 7 8 9 10 |
... <Fragment> <Columns className='is-mobile is-centered'> ... </Columns> <Music/> <Music/> <Music/> </Fragment> ... |
Player de música (exibindo as músicas reais)
1 – No component Musics vamos gerar os components Music com valores reais e também vamos incluir o estado playing que serve para guardarmos a música que está sendo tocado agora:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
import React, { Fragment, useState, useEffect } from 'react'; const Musics = (props) => { const [songs, setSongs] = useState([]); const [playing, setPlaying] = useState([]); useEffect(() => { setSongs(props.songs.map((song, key) => <Music song={song} playing={playing.id == song.id} setPlaying={setPlaying} key={key} artist_name={props.artist_name} /> )); }, [props.songs, playing]); ... <Fragment> <Columns className='is-mobile is-centered'> ... </Columns> {songs} </Fragment> ... |
2 – Para exibir as informações que foram passados para Music, altere ele para:
1 2 3 4 5 6 7 8 |
... <Heading size={5} className='has-text-white'> {props.song.title} </Heading> <CustomSubHeading size={6} className='has-text-white' subtitle> {props.song.artist_name} </CustomSubHeading> ... |
3 – Alterar o serializer albums show para incluir o artist_name
1 2 3 |
... json.artist_name @album.artist.name ... |
4 – Vamos alterar o icone quando a música estiver tocando, em Music coloque:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... let PlayingButton; if(props.playing) { PlayingButton = <FaStopCircle size='45px' className='has-text-white' onClick={() => props.setPlaying([])}/> } else { PlayingButton = <FaPlayCircle size='45px' className='has-text-white' onClick={() => props.setPlaying(props.song)}/> } ... <Fragment> <Columns className='is-vcentered is-mobile is-centered'> <Columns.Column desktop={{size: 1}} mobile={{size: 2}} > {PlayingButton} </Columns.Column> ... |
Player de música (Tocando as músicas)
0 – Em routes.rb atualize a rota do react:
1 |
get "*path", to: "home#index", :constraints => lambda{|req| req.path !~ /\.(png|jpg|js|css|json|mp3)$/ } |
1 – Em Musics vamos incluir um player de áudio (depois vamos oculta-lo):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<Fragment> <Columns className='is-mobile is-centered'> <Columns.Column desktop={{size: 2}} mobile={{size: 12}} className='has-text-centered'> <PlaySequenceButton ... </PlaySequenceButton> <audio controls> <source src={playing.file_url} /> </audio> </Columns.Column> </Columns> {songs} </Fragment> |
2 – Vamos incluir nele um useEffect e o Ref para recarregar o player quando uma música for selecionada:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
... const AudioRef = useRef(); ... useEffect(() => { if (AudioRef.current !== null) { AudioRef.current.pause(); AudioRef.current.load(); if(playing.id) { AudioRef.current.play(); } } }, [playing]); ... <audio controls ref={AudioRef}> <source src={playing.file_url} /> </audio> ... |
3 – Vamos incluir o toque aleatório das músicas:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
... const [playRandom, setPlayRandom] = useState(false); const NextSong = () => { if(playRandom) { let index = Math.floor(Math.random() * props.songs.length); setPlaying(props.songs[index]); } else setPlaying([]); } const SwitchRandom = () => { if(playRandom) { setPlayRandom(false); setPlaying([]); } else setPlayRandom(true); } useEffect(() => { if(playRandom) NextSong(); }, [playRandom]); ... <PlaySequenceButton className='is-medium' color='primary' outlined onClick={() => SwitchRandom()} > {playRandom == true ? 'Parar de tocar' : 'Tocar aleatoriamente'} </PlaySequenceButton> ... |
4 – Agora para fazer a chamada da próxima música:
1 2 3 4 5 6 7 8 9 10 11 |
<Fragment> <Columns className='is-mobile is-centered'> ... <audio controls ref={AudioRef} onEnded={() => NextSong()}> <source src={playing.file_url} /> </audio> ... </Columns.Column> </Columns> ... </Fragment> |
5 – Depos de testar esconda o Player:
1 2 3 4 5 |
... <audio controls ref={AudioRef} onEnded={() => NextSong()} className='is-hidden'> <source src={playing.file_url} /> </audio> ... |
Marcando uma música com ouvida
1 – Criar o service recently_heards.js e colocar:
1 2 3 4 5 6 7 |
import Api from './api'; const RecentlyHeardsService = { create: (id) => Api.post(`/albums/${id}/recently_heards`), } export default RecentlyHeardsService; |
2 – No json de retorno do search incluir o album id na song
1 |
json.album_id song.album.id |
3 – No json de retorno do album incluir o album id na song
1 |
json.album_id song.album.id |
4 – No controller dashboard atualize o load das músicas ouvidas recentemente
1 |
@recent_albums = current_user.recently_heards.order("created_at DESC").limit(8).map(&:album).uniq |
5 – No components Music inclua:
1 2 3 4 5 6 7 8 9 10 |
... import RecentlyHeardsService from '../../services/recently_heards'; ... if (AudioRef.current !== null) { ... if(playing.id) { ... RecentlyHeardsService.create(playing.album_id) } } |
Incluindo o Favorites
-
123def is_favorite? kind, idself.favorites.where(favoritable_type: kind, favoritable_id: id).present?end
-
No jbuilder show do Album inclua dentro do nó songs:
123456...json.songs @album.songs.each do |song|...json.favorite current_user.is_favorite? 'Song', song.id...end -
No Jbuilder index do search inclua no nó song:
1json.favorite current_user.is_favorite? 'Song', song.id -
No Jbuilder index do favorites inclua no nó song:
1json.favorite current_user.is_favorite? 'Song', song.id -
Em /common crie o component Favorite (favorite/index.js):
12345678910import React, { Fragment } from 'react';const Favorite = (props) => {return (<Fragment></Fragment>);}export default Favorite;6 – Em services crie o service favorites e coloque:
123456789import Api from './api';const FavoritesService = {index: () => Api.get('/favorites'),create: (kind, id) => Api.post(`/${kind}/${id}/favorite`),delete: (kind, id) => Api.delete(`/${kind}/${id}/favorite`)}export default FavoritesService;7 – No nosso Favorite vamos adicionar o botão de favoritar e desfavoritar e o state que controla isso:
123456789101112131415161718192021import React, { Fragment, useState } from 'react';...import { FaRegHeart, FaHeart } from "react-icons/fa";...const Favorite = (props) => {const [favored, setFavored] = useState(props.favored);...let FavoredButton;if(favored)FavoredButton = <FaHeart size='25px' className='has-text-white'/>elseFavoredButton = <FaRegHeart size='25px' className='has-text-white'/>return (<Fragment>{ FavoredButton }</Fragment>);}8 – Vamos adicionar o nosso service Favorites e os métodos para chama-lo:
12345678910111213...import FavoritesService from '../../../services/favorites';...async function disfavor() {await FavoritesService.delete(props.kind, props.id);setFavored(false);}async function favor() {await FavoritesService.create(props.kind, props.id);setFavored(true);}9 – Vamos incluir a chamada para esses métodos nos botões:
12345let FavoredButton;if(favored)FavoredButton = <FaHeart size='25px' className='has-text-white' onClick={() => disfavor()}/>elseFavoredButton = <FaRegHeart size='25px' className='has-text-white' onClick={() => favor()}/>10 – Em Music importe o Favorite e exiba:
1234567891011121314151617181920...import Favorite from '../../common/favorite';...<Fragment><Columns className='is-vcentered is-mobile is-centered'><Columns.Column desktop={{size: 1}} mobile={{size: 2}} >...</Columns.Column><Columns.Column desktop={{size: 4}} mobile={{size: 8}}><Columns className='is-vcentered is-mobile'>...<Columns.Column desktop={{size: 4}} mobile={{size: 4}} className='is-pulled-right has-text-right'><Favorite id={props.song.id} kind='songs' favored={props.song.favorite}/></Columns.Column></Columns>...</Columns.Column></Columns></Fragment>
Criando a página de Search
1 – Crie um component chamado Search e coloque nele:
1 2 3 4 5 6 7 8 9 10 |
import React, { Fragment, useEffect, useState } from 'react'; const Search = () => { return ( <Fragment> </Fragment> ); } export default Search; |
2 – Na Screen Search coloque:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import React, { Fragment } from 'react'; import SectionWrapper from '../../components/common/section_wrapper' import { Heading } from 'react-bulma-components'; import Search from '../../components/search'; import NavbarFooter from '../../components/common/navbar_footer'; const SearchScreen = () => { return ( <Fragment> <SectionWrapper> <Heading className='has-text-centered has-text-white'>Buscar</Heading> <Search/> </SectionWrapper> <NavbarFooter/> </Fragment> ); } export default SearchScreen; |
3 – Dentro da pasta do component Search crie um component chamado SearchBar e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 |
import React, { Fragment, useEffect, useState } from 'react'; const SearchBar = () => { return ( <Fragment> </Fragment> ); } export default SearchBar; |
4 – Dentro de /common crie um component chamado Categories e coloque nele:
1 2 3 4 5 6 7 8 9 10 |
import React, { Fragment, useEffect, useState } from 'react'; const Categories = () => { return ( <Fragment> </Fragment> ); } export default Categories; |
5 – Importe esses components em Search
1 2 3 4 5 |
import React, { Fragment, useEffect, useState } from 'react'; import SearchBar from './search_bar'; import Categories from '../common/categories'; import { Columns } from 'react-bulma-components'; ... |
4 – Inclua a estrutura agora
1 2 3 4 5 6 7 8 9 10 11 12 |
... <Fragment> <Columns> <Columns.Column desktop={{ size: 6, offset: 3 }} mobile={{ size: 12 }}> <SearchBar/> </Columns.Column> </Columns> <Categories/> </Fragment> ... |
Criando a busca por texto e categoria
1 – Crie o service Search e coloque nele:
1 2 3 4 5 6 7 |
import Api from './api'; const SearchService = { index: (query) => Api.get(`/search?query=${query}`) } export default SearchService; |
2 – Crie um service chamado Categories e coloque nele:
1 2 3 4 5 6 7 |
import Api from './api'; const CategoriesService = { show: (id) => Api.get(`/categories/${id}`) } export default CategoriesService; |
3 – No component Search inclua os services:
1 2 3 4 |
... import SearchService from '../../services/search'; import CategoriesService from '../../services/categories'; ... |
4 – Agora inclua os hooks:
1 2 3 4 5 |
... const [albums, setAlbums] = useState([]); const [artists, setArtists] = useState([]); const [songs, setSongs] = useState([]); ... |
5 – Inclua o método de busca por categoria:
1 2 3 4 5 6 |
async function fetchCategorySearch(id) { const response = await CategoriesService.show(id); setAlbums(response.data['albums']); setArtists(response.data['artists']); setSongs(response.data['songs']); } |
6 – Inclua o método de busca por texto:
1 2 3 4 5 6 |
async function fetchSearch(query) { const response = await SearchService.index(query); setAlbums(response.data['albums']); setArtists(response.data['artists']); setSongs(response.data['songs']); } |
7 – Atualize a chamada do SearchBar e do Categories colocando:
1 2 3 4 5 6 7 8 9 10 11 |
... <Fragment> <Columns> <Columns.Column desktop={{ size: 6, offset: 3 }} mobile={{ size: 12 }}> <SearchBar fetchSearch={fetchSearch}/> </Columns.Column> </Columns> <Categories fetchCategorySearch={fetchCategorySearch}/> </Fragment> ... |
Criando os components Categories e SearchBar
1 – No component SearchBar vamos incluir a estrutura:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
import React, { Fragment, useState } from 'react'; import { Form } from 'react-bulma-components'; const SearchBar = (props) => { return ( <Fragment> <Form.Field> <Form.Control iconRight> <Form.Input placeholder="Text input" placeholder='Álbuns, artistas ou músicas' /> </Form.Control> </Form.Field> </Fragment> ); } export default SearchBar; |
2 – Vamos incluir o state query e o método de pesquisa:
1 2 3 4 5 6 7 8 9 10 |
... const SearchBar = (props) => { const [query, setQuery] = useState(""); const Search = (e) => { if (e.key === 'Enter') { props.fetchSearch(query); } } ... |
3 – Agora vamos atualizar o nosso form input:
1 2 3 4 5 6 7 8 9 10 11 12 |
... <Form.Field onKeyDown={Search}> <Form.Control iconRight> <Form.Input placeholder="Text input" placeholder='Álbums, artistas ou músicas' value={query} onChange={e => setQuery(e.target.value)} /> </Form.Control> </Form.Field> ... |
4 – No service Categories inclua:
1 2 3 |
... index: () => Api.get('/categories'), ... |
5 – No component Category vamos incluir o state e o método para baixar as categorias:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... import CategoriesService from '../../../services/categories'; import { Columns, Image, Section } from 'react-bulma-components'; const Categories = (props) => { const [categories, setCategories] = useState([]); async function fetchCategories() { const response = await CategoriesService.index(); setCategories(response.data['categories']) } useEffect(() => { fetchCategories(); }, []); ... |
6 – Vamos gerar uma lista de imagens baseado nas categorias que recebemos:
1 2 3 4 5 |
const categories_list = categories.map((category, key) => <Columns.Column desktop={{size: 3}} mobile={{size: 6}} key={key}> <Image src={category.image_url} onClick={() => props.fetchCategorySearch(category.id)}></Image> </Columns.Column> ); |
7 – Vamos criar uma Div customizada:
1 2 3 4 5 6 7 |
... import styled from 'styled-components'; const DivVSpaced = styled.div` margin-top: 50px; ` ... |
8 – Vamos injetar agora as categorias no template:
1 2 3 4 5 6 7 |
<Fragment> <DivVSpaced> <Columns className='is-mobile'> {categories_list} </Columns> </DivVSpaced> </Fragment> |
Criando a Tab de resultados
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import React, { Fragment, useState, useEffect } from 'react'; import { Columns, Tabs } from 'react-bulma-components'; import styled, {css} from 'styled-components' const ResultsTabs = (props) => { return ( <Fragment> </Fragment> ); } export default ResultsTabs; |
2 – Importe o component Tabs no Search e injete ele:
1 2 3 4 5 6 7 8 9 10 11 12 |
... import ResultsTabs from '../common/results_tabs' ... <Fragment> <Columns> ... </Columns> <ResultsTabs albums={albums} artists={artists} songs={songs}/> ... </Fragment> |
3 – Vamos incluir o template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
<Fragment> <Tabs align='centered' size='medium'> <Tabs.Tab> Álbums </Tabs.Tab> <Tabs.Tab> Artistas </Tabs.Tab> <Tabs.Tab> Músicas </Tabs.Tab> </Tabs> <div> <div> <Columns className="columns is-mobile is-multiline"> Álbums </Columns> </div> <div> <div className="columns is-mobile is-multiline"> Artists </div> </div> <div> <div className="columns is-multiline"> <div className="column is-12"> Musics </div> </div> </div> </div> </Fragment> |
4 – Vamos customizar o component Tab:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
... import styled, {css} from 'styled-components' const CustomTab = styled(Tabs.Tab)` a{ color: white; ${({ active }) => active && css` color: hsl(171, 100%, 41%) !important; border-color: hsl(171, 100%, 41%) !important; `} } ` ... <Tabs align='centered' size='medium'> <CustomTab> Álbuns </CustomTab> <CustomTab> Artistas </CustomTab> <CustomTab> Músicas </CustomTab> </Tabs> ... |
5 – Vamos incluir o controlle das Tabs:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
... const ResultsTabs = (props) => { const [active_tab, setActiveTab] = useState("albums"); .. <Fragment> <Tabs style={{display: props.albums.length || props.artists.length || props.songs.length ? "" : "none"}} align='centered' size='medium' > <CustomTab active={active_tab == 'albums' ? true : false} onClick={() => setActiveTab('albums')}> Álbums </CustomTab> <CustomTab active={active_tab == 'artists' ? true : false} onClick={() => setActiveTab('artists')}> Artistas </CustomTab> <CustomTab active={active_tab == 'songs' ? true : false} onClick={() => setActiveTab('songs')}> Músicas </CustomTab> </Tabs> <div> <div style={{display: active_tab != 'albums' ? "none" : ""}}> <Columns className="columns is-mobile is-multiline"> Albuns </Columns> </div> <div style={{display: active_tab != 'artists' ? "none" : ""}}> <div className="columns is-mobile is-multiline"> Artists </div> </div> <div style={{display: active_tab != 'songs' ? "none" : ""}}> <div className="columns is-multiline"> <div className="column is-12"> Songs </div> </div> </div> </div> </Fragment> |
6 – Vamos gerar os components Album e injetar no template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
... import Album from '../../common/album'; ... const [albums, setAlbums] = useState([]); ... useEffect(() => { setAlbums(props.albums.map((album, key) => <Columns.Column desktop={{ size: 3 }} mobile={{ size: 6 }} key={key}> <Album artist_name={album.artist_name} title={album.title} cover_url={album.cover_url} id={album.id}/> </Columns.Column> )); }, [props.albums, props.artist_name, props.songs]); ... <div> <div style={{display: active_tab != 'albums' ? "none" : ""}}> <Columns className="columns is-mobile is-multiline"> {albums} </Columns> </div> ... |
7 – Vamos importar o component Music e injetar ele:
1 2 3 4 5 6 7 8 9 10 11 |
... import Musics from '../../musics'; ... <div style={{display: active_tab != 'songs' ? "none" : ""}}> <div className="columns is-multiline"> <div className="column is-12"> <Musics songs={props.songs || []}/> </div> </div> </div> ... |
Criando a página de favoritos
1 2 3 4 5 6 7 8 9 10 11 |
import React, { Fragment, useState, useEffect } from 'react'; const Favorites = () => { return ( <Fragment> </Fragment> ); } export default Favorites; |
2 – Em favorite Screen importe o component e acrescente ao template:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import React, { Fragment } from 'react'; import NavbarFooter from '../../components/common/navbar_footer'; import SectionWrapper from '../../components/common/section_wrapper'; import { Heading } from 'react-bulma-components'; import Favorites from '../../components/favorites'; const FavoritesScreen = () => { return( <Fragment> <SectionWrapper> <Heading className='has-text-centered has-text-white'>Favoritos</Heading> <Favorites/> </SectionWrapper> <NavbarFooter/> </Fragment> ); } export default FavoritesScreen; |
3 – No component Favorite vamos incluir o state favorites:
1 |
const [favorites, setFavorites] = useState({}); |
4 – Inclua o service Favorites e os métodos para baixar os favorites:
1 2 3 4 5 6 7 8 9 10 11 12 |
... import FavoritesService from '../../services/favorites'; ... async function fetchFavorites() { const response = await FavoritesService.index(); await setFavorites(response.data); } useEffect(() => { fetchFavorites(); }, []); |
5 – Agora coloque o resultado no template:
1 2 3 4 5 6 7 |
... import ResultsTabs from '../common/results_tabs'; ... <Fragment> <ResultsTabs albums={favorites.albums || []} artists={favorites.artists || []} songs={favorites.songs || []}/> </Fragment> |
Transformando o APP em PWA
1 – Vamos liberar o Cors, no gemfile coloque:
1 |
gem 'rack-cors' |
2 – Rode;
1 |
bundle install |
3 – Em initializers crie um arquivo chamado Cors e coloque nele:
1 2 3 4 5 6 7 8 9 |
Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins '*' resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end |
4 – Vamos ajustar a URL base dos services, em api.js coloque:
1 2 3 |
import axios from 'axios'; const Api = axios.create({baseURL: '/api/v1'}); export default Api; |
5 – Coloque no Gemfile:
1 |
gem 'serviceworker-rails' |
6 – Rode no console:
1 |
bundle install |
7 – Rode no terminal:
1 |
rails g serviceworker:install |
8 – Em app/assets/javascripts customize o manifeste.json:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<% icon_sizes = Rails.configuration.serviceworker.icon_sizes %> { "name": "SpotcodeV2", "short_name": "SpotcodeV2 - Music Play", "start_url": "/", "icons": [ <% icon_sizes.map { |s| "#{s}x#{s}" }.each.with_index do |dim, i| %> { "src": "<%= image_path "serviceworker-rails/heart-#{dim}.png" %>", "sizes": "<%= dim %>", "type": "image/png" }<%= i == (icon_sizes.length - 1) ? '' : ',' %> <% end %> ], "theme_color": "#333333", "background_color": "#222222", "display": "fullscreen", "orientation": "portrait" } |
9 – Em application.html.erb coloque:
1 |
<link rel="manifest" href="/manifest.json" /> |
10 – Em initializers/assets coloque:
1 |
Rails.application.config.assets.precompile += %w[serviceworker.js manifest.json] |
11 – Em javascript/packs/application.js coloque:
1 |
import '../../assets/javascripts/serviceworker-companion.js'; |
12 – Baixe o ngrok: https://ngrok.com
13 – Adicione em config/application.rb:
1 |
config.hosts.clear |
Desafios
1 – Criar o component Artist
-
Exibir ele na busca
-
Exibir ele nos favoritos
2 – Criar a página Artist
-
Mostrar os detalhes do artista
-
Mostrar todas as músicas
-
Mostrar todos os albums
3 – Permitir o favorite em Artist e Album
4 – (Desafio Hard) Incluir um player mais robusto: https://github.com/lhz516/react-h5-audio-player#readme