Aula 2: Front-end com Next.js
Introdução ao Cliente Web: 00:01:13
Criação do projeto: 00:05:25
Instalação de dependências: 00:13:39
Estrutura do projeto e Estilos globais: 00:16:57
Component Header: 00:32:14
Tela Home: 00:45:57
Efeito de digitação: 00:58:09
Listando Restaurantes e Categorias: 01:10:46
Baixando os restaurantes da API: 01:33:37
Desafio dos Reviews: 01:50:57
Lista de Categorias: 01:52:50
Baixando as categorias da API: 02:10:07
Criando Busca: 02:19:58
Tela de Detalhes do Restaurante: 02:33:42
Instalando o Recoil: 02:55:36
Desafio Friendly id: 03:04:06
[Aviso importante: Caso você copie e cole os códigos, alguns caracteres invisíveis podem vir junto e dar alguns Bugs, então remova os espaços entre as linhas (e os crie novamente com a barra)]
Bons códigos 🤘
Sumário
0 – Links importantes
- Guia para instalar as dependências: https://onebitcode.com/guia-de-instalacao-do-ruby-on-rails
- API completa no GitHub: https://github.com/OneBitCodeBlog/onebitfoodV2-api
- Cliente Web no GitHub: https://github.com/OneBitCodeBlog/onebitfoodV2-nextjs
A ideia inicial
Criar um APP inspirado no Ifood usando Ruby On Rails como API e Next.js como cliente Web
-
Mockups
-
Modelo do banco de dados
-
Ferramenta:
-
-
Documentação dos endpoints
-
Ferramenta: https://www.postman.com
-
Material: https://documenter.getpostman.com/view/10378249/TzRPk9yD
-
Dependências
A seguir, veja as dependências para este projeto (cliente Web)
-
-
-
React-bootstrap 1.5.2
-
Bootstrap 5.0.0
-
Swr 0.5.5
-
Sass 1.32.8
-
Recoil ^0.2.0
-
Recoil Persist ^2.5.0
-
Slick Carousel ^1.8.1
Nesta aula nós vamos gerar o nosso projeto básico usando o Create React APP.
1 – Crie um novo projeto ReactJS. Para isso abra seu terminal e digite:
1 |
npx create-next-app onebitfood-client |
2 – Após isso, acesse o diretório do projeto.
1 |
cd onebitfood-client |
3 – Para testar que seu projeto funcionou rode:
1 |
npm run dev |
4 – Apague o arquivo styles/Home.module.css
5 – Atualize o arquivo _app.js:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
import Head from 'next/head'; import '../styles/globals.scss'; export default function MyApp({ Component, pageProps }) { return( <> <Head> <title>OneBitFood V2</title> </Head> <main> <Component {...pageProps} /> </main> </> ) } |
5 – Limpe o arquivo pages/index.js :
1 2 3 4 5 6 |
export default function Home() { return ( <> </> ) } |
Assim teremos um código menos verboso nesse inicio removendo arquivos que não serão utilizados.
6 – Atualize no arquivo package.json o script dev:
1 |
"dev": "next dev -p 3001", |
Nesta aula nós vamos instalar as principais dependências do projeto como Swr para as chamadas web, sass para utilizadas o sass ao invés do css comum e o Bootstrap para a estilização do projeto.
1 – Instale também o Swr para fazermos requisições em Apis:
1 |
yarn add swr@0.5.5 |
2 – Instale o node-sass, para que possamos adicionar SCSS no nosso projeto
1 |
yarn add sass@1.32.12 |
3 – Instale o framework CSS Bootstrap
1 |
yarn add react-bootstrap@1.5.2 bootstrap@5.0.0 |
4 – Instale a biblioteca de ícones:
1 |
yarn add react-icons@4.2.0 |
Para manter a organização vamos criar a estrutura principal de diretórios do nosso projeto já no inicio e também vamos criar alguns estilos comuns a todos os elementos do SPA como cores, espaçamentos e imagem de background.
1 – Crie a de components do nosso projeto:
1 |
mkdir components |
2 – Baixe as seguintes imagens e as coloque dentro da pasta public
: https://drive.google.com/drive/folders/1-iHaCLfuyYktwen46bOMnxSF3dRD22XD?usp=sharing
3 – Limpe o conteúdo do arquivo styles/globals.css e renomeie ele para globals.scss
4 – Atualize o arquivo pages/_app.js colocando:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import Head from 'next/head'; import '../styles/globals.scss'; export default function MyApp({ Component, pageProps }) { return( <> <Head> <title>OneBitFood V2</title> <link rel="icon" href="/favicon.ico" /> </Head> <main> <Component {...pageProps} /> </main> </> ) } |
5 – Crie um arquivo chamado styles/colors.scss coloque nele:
1 2 3 4 5 |
$custom-red: #FF0043; $custom-gray: #cccccc; $custom-gray-darker: #666666; $custom-black: #353535; $custom-orange: #F7C97C; |
6 – Em globals.scss coloque:
1 2 3 4 5 6 7 8 9 |
@import "colors.scss"; $theme-colors: ( "custom-red": $custom-red, "custom-gray": $custom-gray, "custom-gray-darker": $custom-gray-darker, "custom-black": $custom-black, "custom-orange": $custom-orange ); |
7 – Também adicione uma extensão da variável spacer do bootstrap para termos mais tamanhos de margins e padings e também o efeito de bold para font-weight 600 disponíveis:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$spacer: 1rem; $spacers: ( 0: 0, 1: ($spacer * .25), 2: ($spacer * .5), 3: $spacer, 4: ($spacer * 1.5), 5: ($spacer * 3), 6: ($spacer * 5), 7: ($spacer * 10), 8: ($spacer * 15), 9: ($spacer * 20) ); $font-weight-bold: 600; |
8 – Agora vamos importar o Bootstrap no mesmo arquivo (globals.scss):
1 |
@import "~bootstrap/scss/bootstrap"; |
Obs: Coloque esse import depois do $spacers.
9 – Para incluir o Background acrescente em globals.scss:
1 2 3 4 5 6 7 8 9 |
html, body { margin: 0px; padding: 0px; height: 100%; } body { background-image: url('../public/bg.png'); } |
Aqui nós incluímos o background fixo da nossa aplicação.
10 – Para mudar definir a fonte padrão inclua a importação da fonte e atualize os estilos do “html, body” incluídos anteriormente em globals.scss:
1 2 3 4 5 6 7 8 |
@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700&display=swap'); html, body { margin: 0px; padding: 0px; height: 100%; font-family: 'Lexend', sans-serif; } |
11 – Para alterar o comportamento da classe container do bootstrap para que o conteúdo ocupe mais espaço lateral na tela no mobile, coloque em globals.scss:
1 |
@media (max-width:1025px) { .container { width: 95% !important;} } |
12 – No arquivo pages/index.js, coloque:
1 2 3 4 5 6 7 8 9 10 11 |
import Container from 'react-bootstrap/Container'; export default function Home() { return ( <> <Container> Home Page Content </Container> </> ) } |
13 – Suba o projeto e vejo o background + bootstrap em ação
1 |
yarn dev |
Nesta parte, vamos criar o componente Header que será utilizado em toda aplicação que será o Header.
1 – Primeiro, vamos criar o diretório src/components/header
e dentro dele o arquivo index.js
1 2 |
mkdir components/Header touch components/Header/index.js |
2 – No arquivo components/Header/index.js` coloque:
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 from 'react'; import { Navbar } from 'react-bootstrap'; import Image from 'next/image'; import Link from 'next/link'; const Header = () => { return ( <Navbar bg="white" expand="lg" className="border-bottom border-custom-gray"> <Navbar.Brand> <Link href="/restaurants"> <a> <Image src="/logo.png" alt="OneBitFood" width={200} height={44} /> </a> </Link> </Navbar.Brand> </Navbar> ) } export default Header; |
3 – Agora vamos adicionar o Header (e o Container) no nosso app. Importe e adicione o componete Header em src/_app.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import Head from 'next/head'; import '../styles/globals.scss'; import Header from "../components/Header"; import Container from 'react-bootstrap/Container'; export default function MyApp({ Component, pageProps }) { return( <> <Head> <title>OneBitFood V2</title> <link rel="icon" href="/favicon.ico" /> </Head> <main> <Header /> <Container className="mt-5"> <Component {...pageProps} /> </Container> </main> </> ) } |
4 – Verifique com ficou subindo o projeto:
1 |
yarn dev |
5 – Para incluir uma classe geral que vai dar destaque ao que for clicável:
a – No globals.scss coloque:
1 2 3 4 5 |
.clickable_effect:hover { opacity: 0.8; transform:scale(1.01); cursor: pointer; } |
b – Na imagem do Header inclua a classe:
1 2 3 4 5 6 7 |
<Image src="/logo.png" alt="OneBitFood" width={200} height={44} className="clickable_effect" /> |
6 – Suba o projeto, passe o mouse sobre o logo e veja o efeito.
1 – Adicione em pages/index.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import { Button, Row, Col } from 'react-bootstrap'; import Link from 'next/link'; import { FaCrosshairs } from 'react-icons/fa'; export default function Home() { return ( <Row className="mt-8 justify-content-center"> <Col md={7} xs={12} className="text-center"> <h1 className='fw-bolder text-custom-gray-darker mb-5 lh-base display-5'> Comida saudável e gostosa direto na sua casa </h1> <Link href='/restaurants'> <Button variant="custom-red" size="lg" className='text-white'> <FaCrosshairs className='pb-1'/> <span className='px-2 fw-bolder'>ENCONTRAR AGORA</span> </Button> </Link> </Col> </Row> ) } |
Vamos incluir na nossa Home um efeito de “digitando” para tornar a experiencia do usuário ainda melhor
1 – Crie um component em components/Typewriter/index.js
2 – Crie a estrutura do Component:
1 2 3 4 5 6 7 8 |
import React from 'react'; export default function Typewriter(props) { return( <></> ) } |
3 – Crie um estado para armazenar o que vai aparecer na página:
1 2 3 4 5 6 7 8 |
import React, { useState } from 'react'; ... export default function Typewriter(props) { const [phrase, setPhrase] = useState("") ... <>{phrase}</> ... |
4 – Inclua um “efeito”:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import React, { useState, useEffect } from 'react'; ... export default function Typewriter(props) { ... useEffect(() => { let currentText = ""; props.text.split('').forEach((char, index) => { setTimeout( () => { currentText = currentText.slice(0, -1) currentText += char; if(props.text.length != index + 1) currentText += "❙" setPhrase(currentText) }, 200 + (index * 100)); }) }, []); |
5 – Atualize o component pages/index.js para colocar o Typewriter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { FaCrosshairs } from 'react-icons/fa'; import { Container, Button, Row, Col } from 'react-bootstrap'; import Link from 'next/link'; import Typewriter from '../components/Typewriter'; export default function Home() { return ( <Row className="mt-8 justify-content-md-center"> <Col md={7} xs={12} className='text-center'> <h1 className='fw-bolder text-custom-gray-darker mb-5 lh-base display-5'> <Typewriter text="Comida saudável e gostosa direto na sua casa"/> </h1> <Link href="/restaurants"> <Button variant="custom-red" size="lg" className='text-white'> <FaCrosshairs/> <span className='px-2'>ENCONTRAR AGORA</span> </Button> </Link> </Col> </Row> ) } |
1- Crie um diretório src/screens/restaurants
e dentro dele o arquivo index.js
1 2 |
mkdir pages/restaurants touch pages/restaurants/index.js |
2 – Crie a estrutura da página:
1 2 3 4 5 6 7 |
export default function Restaurants() { return ( <> </> ) } |
3 – Agora vamos criar um componente para a lista de restaurantes desta tela:
1 2 |
mkdir components/ListRestaurants touch components/ListRestaurants/index.js |
4 – Adicione o seguinte conteúdo no component criado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import React from 'react'; import { Container, Row, Col } from 'react-bootstrap'; export default function ListRestaurants() { return ( <div className='mt-5'> <h3 className='fw-bold'>Restaurantes</h3> <Row> <Col>Restaurants...</Col> </Row> </div> ) } |
5 – Volte no arquivo pages/restaurants/index.js
e altere o conteúdo para:
1 2 3 4 5 6 7 8 9 |
import ListRestaurants from "../../components/ListRestaurants"; export default function Restaurants() { return ( <> <ListRestaurants /> </> ) } |
6 – Agora vamos criar dois métodos úteis que serão reutilizados depois, o toCurrency para formatar o valores financeiros e o trucanteString para exibir no máximo X caracteres de uma string.
a – Crie uma pasta chamada services, rodando:
1 |
mkdir services |
b – Dentro dela crie o arquivo toCurrency.js e coloque nele:
1 2 3 4 5 6 7 |
export default function toCurrency(value) { const formatter = new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL', }) return formatter.format(value) } |
c – Crie dentro de services também o arquivo trucanteString.js e coloque nele:
1 2 3 4 5 6 |
export default function truncateString(str, num) { if (str.length <= num) return str else return str.slice(0, num) + '...' } |
7 – Vamos criar o component restaurant que será o box usado para mostrar as informações de cada restaurant no listagem
1 2 |
mkdir components/ListRestaurants/Restaurants touch components/ListRestaurants/Restaurants/index.js |
8 – Adicione o seguinte conteúdo no component criado:
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 45 46 |
import React from 'react'; import { Row, Col, Card } from 'react-bootstrap'; import { FaStar } from 'react-icons/fa'; import Image from 'next/image' import Link from 'next/link' import toCurrency from '../../../services/toCurrency'; import truncateString from '../../../services/truncateString'; const Restaurant = (props) => ( <Col lg={6} sm={6} xs={12} className="mb-4"> <Link href={`restaurants/${props.id}`}> <Card body className='clickable_effect'> <Row> <Col md={5} xs={12}> <Image src={props.image_url} alt={props.name} width={300} height={200} layout="responsive" /> </Col> <Col md={5} xs={10}> <h5>{truncateString(props.name, 25)}</h5> <p className='mb-1'> <small> {truncateString(props.description, 60)} </small> </p> <p> <small className='fw-bold'>{props.category_title}</small> </p> <small className='border px-3 border-custom-gray fw-bold'> entrega {toCurrency(props.delivery_tax)} </small> </Col> <Col md={2} xs={2} className="text-center"> <span className='text-custom-orange'> <FaStar/> 5 </span> </Col> </Row> </Card> </Link> </Col> ) export default Restaurant; |
9 – Atualize o component src/components/ListRestaurants/index.js para renderizarmos uma lista de restaurants:
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 45 46 47 48 49 |
import React from 'react'; import { Container, Row, Col } from 'react-bootstrap'; import Restaurant from '../ListRestaurants/Restaurant'; export default function ListRestaurants() { const restaurants = [ { 'id': 1, 'name': 'example 1', 'description': 'Javascript Ipsum, Javascript Ipsum e Javascript Ipsum', 'delivery_tax': '5', 'image_url': '/restaurant.jpeg', 'category_title': 'Cozinha japonesa' }, { 'id': 2, 'name': 'example 2', 'delivery_tax': '10', 'description': 'Javascript Ipsum, Javascript Ipsum e Javascript Ipsum', 'image_url': '/restaurant.jpeg', 'category_title': 'Cozinha mineira' }, { 'id': 3, 'name': 'example 3', 'delivery_tax': '15', 'description': 'Javascript Ipsum, Javascript Ipsum e Javascript Ipsum', 'image_url': '/restaurant.jpeg', 'category_title': 'Cozinha vegana' }, { 'id': 4, 'name': 'example 4', 'delivery_tax': '10', 'description': 'Javascript Ipsum, Javascript Ipsum e Javascript Ipsum Javascript Ipsum, Javascript Ipsum e Javascript Ipsum', 'image_url': '/restaurant.jpeg', 'category_title': 'Cozinha vegana' } ] return ( <div className='mt-5'> <h3 className='fw-bold'>Restaurantes</h3> <Row> {restaurants.map((restaurant, i) => <Restaurant {...restaurant} key={i}/>)} </Row> </div> ) } |
10 – Veja o que foi feito subindo o projeto e acessando /restaurants:
1 |
yarn dev |
1 – Uma pequena correção, na API, na partial _restaurante acrescente (faltou devolvermos esse campo importante):
1 |
json.category_title restaurant.category.title |
2 – Crie um arquivo na raiz do projeto chamado next.config.js e coloque nele:
1 2 3 4 5 6 7 8 |
module.exports = { images: { domains: ['localhost'], }, env: { apiUrl: 'http://localhost:3000', }, } |
ps: Reinicie o servidor se ele estiver ativo
3 – Para fazer as chamadas do restaurante crie um service chamado getRestaurants.js e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import useSWR from 'swr'; export default function getRestaurants() { const fetcher = (...args) => fetch(...args).then((res) => res.json()); const { data, error } = useSWR( `${process.env.apiUrl}/api/restaurants`, fetcher, { revalidateOnFocus: false } ) return { restaurants: data, isLoading: !error && !data, isError: error } } |
4 – No component ListRestaurants inclua essa chamada atualizando o conteúdo dele para:
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 from 'react'; import { Row, Col, Spinner, Alert } from 'react-bootstrap'; import Restaurant from '../ListRestaurants/Restaurant'; import getRestaurants from '../../serivces/getRestaurants'; export default function ListRestaurants() { const { restaurants, isLoading, isError } = getRestaurants(); function renderContent() { if(isError) return <Col><Alert variant='custom-red'>Erro ao carregar</Alert></Col> else if(isLoading) return <Col><Spinner animation='border'/></Col> else if(restaurants.lenght == 0) return <Col>Nenhum restaurante disponível ainda...</Col> else return restaurants.map((restaurant, i) => <Restaurant {...restaurant} key={i}/>) } return ( <div className='mt-5'> <h3 className='fw-bold'>Restaurantes</h3> <Row> {renderContent()} </Row> </div> ) } |
1 – Crie uma tabela na API para armazenar os reviews que o restaurante recebeu.
2 – Associe essa tabela ao resturante.
3 – Popule ela com reviews fake nos seeds.
4 – Devolva a nota média do restaurante junto com os outros dados dele.
Nesta aula nós vamos desenvolver o component que mostrada a lista de categorias para filtrarmos os restaurantes.
1 – Crie uma pasta src/components/categories
e o arquivo index.js
para o componente Categories
1 2 |
mkdir components/Categories touch components/Categories/index.js |
2 – Para este componentes, primeiro é necessário instalar o plugin Slick Carousel e seu sua adaptação para o React
1 |
yarn add slick-carousel react-slick |
3 – Dentro da pasta src/components/categories
crie um arquivo slick_settings.js
para as configurações do Slick com o seguinte conteúdo
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 |
export default { speed: 500, slidesToShow: 5, slidesToScroll: 4, initialSlide: 0, dots: false, infinite: false, responsive: [ { breakpoint: 1024, settings: { slidesToShow: 3, slidesToScroll: 3, } }, { breakpoint: 600, settings: { slidesToShow: 2, slidesToScroll: 2, initialSlide: 2 } }, { breakpoint: 480, settings: { slidesToShow: 2, slidesToScroll: 1, dots: true } } ] }; |
4 – No component categories, crie a base colocando:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import Slider from "react-slick"; import "slick-carousel/slick/slick.css"; import "slick-carousel/slick/slick-theme.css"; import { Card, Container, Row, Col } from 'react-bootstrap'; import slickSettings from "./slick_settings"; export default function Categories() { return ( <> <h3 className='fw-bold'>Categorias</h3> <Card className="mt-2"> <Slider {...slickSettings} className="px-4 pt-4 text-center"> </Slider> </Card> </> ) } |
5 – Atualize a screen Restaurants pages/restaurants/index.js
1 2 3 4 5 6 7 8 9 10 11 |
import ListRestaurants from "../../components/ListRestaurants"; import Categories from "../../components/Categories"; export default function Restaurants() { return ( <> <Categories/> <ListRestaurants /> </> ) } |
7 – Crie o component Category (components/categories/Category/index.js) e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import Link from 'next/link'; import Image from 'next/image'; export default function Category(props) { return( <div> <Link href={`/restaurants?category=${props.title}`}> <a> <Image src={props.image_url} alt={props.title} width={300} height={200} className='px-1 clickable_effect' /> </a> </Link> <p className='fw-bold'>{props.title}</p> </div> ) } |
8 – Atualize o components Categories importando o Category e passando dados Fake:
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 |
import Slider from "react-slick"; import "slick-carousel/slick/slick.css"; import "slick-carousel/slick/slick-theme.css"; import { Card, Container, Row, Col } from 'react-bootstrap'; import slickSettings from "./slick_settings"; import Category from './Category'; export default function Categories() { const categories = [{ 'id': 1, 'title': 'Italiana', 'image_url': '/category.jpeg' },{ 'id': 1, 'title': 'Italiana', 'image_url': '/category.jpeg' },{ 'id': 1, 'title': 'Italiana', 'image_url': '/category.jpeg' },{ 'id': 1, 'title': 'Italiana', 'image_url': '/category.jpeg' },{ 'id': 1, 'title': 'Italiana', 'image_url': '/category.jpeg' } ] return ( <> <h3 className='fw-bold'>Categorias</h3> <Card className="mt-2"> <Slider {...slickSettings} className="px-4 pt-4 text-center"> {categories.map((category, i) => <Category {...category} key={i}/>)} </Slider> </Card> </> ) } |
9 – Veja como ficou:
1 |
yarn dev |
Assim como fizemos com restaurantes na aula passada, agora vamos conectar a API para carregar as categorias
1 – Crie um service chamado getCategories.js e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 |
import useSWR from 'swr'; export default function getCategories() { const fetcher = (...args) => fetch(...args).then((res) => res.json()) const { data, error } = useSWR(`${process.env.apiUrl}/api/categories`, fetcher, { revalidateOnFocus: false } ) return { categories: data, isLoading: !error && !data, isError: error } } |
2 – No component Categories:
a – Importe o getCategories:
1 |
import getCategories from '../../serivces/getCategories'; |
b – Remova a const categories.
c – Inclua a chamada ao getCategories no component:
1 |
const { categories, isLoading, isError } = getCategories(); |
d – Crie um método para renderizar o erro, a mensagem de carregando e o conteúdo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function renderContent() { if(isError) return <Alert variant='custom-red'>Erro ao carregar</Alert>; else if(isLoading) return <Spinner animation="border" />; else { if(categories.length == 0) return <p>Nenhuma categoria disponível ainda</p>; else { return ( <Card className="mt-2"> <Slider {...slickSettings} className="px-4 pt-4 text-center"> {categories.map((category, i) => <Category category={category} key={i}/>)} </Slider> </Card> ) } } } |
e – Atualize as importações dos elementos do bootstrap:
1 |
import { Card, Container, Spinner, Alert } from 'react-bootstrap'; |
f – Atualize o conteúdo do método return:
1 2 3 4 5 6 |
return ( <> <h3 className='fw-bold'>Categorias</h3> {renderContent()} </> ) |
3 – Atualize o getRestaurants.js para permitir o filtro por categoria:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import useSWR from 'swr'; import { useRouter } from 'next/router'; const fetcher = (...args) => fetch(...args).then((res) => res.json()) export default function <span class="md-plain">getRestaurants</span>() { const router = useRouter(); const { category } = router.query; let params = ''; if(category) params = `${params == '' ? '?' : '&'}category=${category}` const { data, error } = useSWR( `${process.env.apiUrl}/api/restaurants${params}`, fetcher, { revalidateOnFocus: false } ) return { restaurants: data, isLoading: !error && !data, isError: error } } |
4 – Teste o APP:
1 |
yarn dev |
Nesta aula vamos criar o componente de busca que vai ficar no cabeçalho do app
1- Crie o diretório components/SearchBox
e o arquivo index.js
1 2 |
mkdir components/SearchBox touch components/SearchBox/index.js |
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 |
import React, { useState } from 'react'; import { Form, Button } from 'react-bootstrap'; import { FaSearch } from 'react-icons/fa'; import { useRouter } from 'next/router'; export default function SearchBox() { const [query, setQuery] = useState("") const router = useRouter() async function Search(event) { event.preventDefault(); router.push(`/restaurants?q=${query}`) } return ( <Form className='d-flex mx-5 my-2' onSubmit={(e) => Search(e)}> <Form.Control type="text" placeholder="Buscar Restaurantes..." value={query} onChange={(e) => setQuery(e.target.value)} className="me-2" /> <Button variant="outline-custom-red" type="submit"> <FaSearch /> </Button> </Form> ) } |
3 – Atualize o código do components/Header/index.js
para
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 |
import React from 'react'; import { Navbar, Nav } from 'react-bootstrap'; import Image from 'next/image'; import Link from 'next/link'; import SearchBox from '../SearchBox'; export default function Header() { return ( <Navbar bg="white" expand="lg" className="border-bottom border-custom-gray"> <Navbar.Brand className="mx-3"> <Link href="/restaurants"> <a> <Image src="/logo.png" alt="OneBitFood" width={200} height={44} className="clickable_effect" /> </a> </Link> </Navbar.Brand> <Navbar.Toggle aria-controls="responsive-navbar-nav" /> <Navbar.Collapse id="responsive-navbar-nav" className="justify-content-end"> <SearchBox /> </Navbar.Collapse> </Navbar> ) } |
4 – Atualize o getRestaurant.js colocando:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import useSWR from 'swr'; import { useRouter } from 'next/router'; const fetcher = (...args) => fetch(...args).then((res) => res.json()) export default function <span class="md-plain">getRestaurants</span>() { const router = useRouter(); const { category, q } = router.query; let params = ''; if(category) params = `${params == '' ? '?' : '&'}category=${category}` if(q) params = `${params == '' ? '?' : `${params}&`}q=${q}` const { data, error } = useSWR( `${process.env.apiUrl}/api/restaurants${params}`, fetcher, { revalidateOnFocus: false } ) return { restaurants: data, isLoading: !error && !data, isError: error } } |
5 – Teste o APP (teste na versão mobile também):
1 |
yarn dev |
Nesta aula vamos criar a tela de visualização do restaurante com os pratos que ele possui
1- Crie uma page para mostrar os detalhes do restaurante em pages/restaurants
, crie um arquivo chamado [id].js
2 – Crie um component para colocar os detalhes do restaurante chamado DetailsRestaurant:
1 2 |
mkdir components/DetailsRestaurant touch components/DetailsRestaurant/index.js |
3 – Na página [id].js coloque a estrutura da página:
1 2 3 4 5 |
import DetailsRestaurant from "../../components/DetailsRestaurant"; export default function Restaurant() { return <DetailsRestaurant />; } |
4 – Para dividirmos melhor a tela de detalhes crie os seguintes components:
1 2 3 4 |
mkdir components/DetailsRestaurant/Details touch components/DetailsRestaurant/Details/index.js mkdir components/DetailsRestaurant/CategoryProducts touch components/DetailsRestaurant/CategoryProducts/index.js |
5 – Crie a base do component DetailsRestaurant/index.js colocando:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import React from 'react'; import Container from 'react-bootstrap/Container'; import Details from './Details'; import CategoryProducts from './CategoryProducts'; export default function DetailsRestaurant(props) { return( <> <Details/> <CategoryProducts/> </> ) } |
6 – Prepare a base dos components que serão usados: a – Details
1 2 3 4 5 6 |
export default function Details(props) { return( <> </> ) } |
b – CategoryProducts
1 2 3 4 5 6 |
export default function CategoryProducts(props) { return( <> </> ) } |
7 – Vamos criar um service para pegar os detalhes do restaurante, em services crie getRestaurant.js e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 |
import useSWR from 'swr'; export default function getRestaurant(id) { const fetcher = (...args) => fetch(...args).then((res) => res.json()); const { data, error } = useSWR( id ? `${process.env.apiUrl}/api/restaurants/${id}` : null, fetcher, { revalidateOnFocus: false } ) return { restaurant: data, isLoading: !error && !data, isError: error } } |
8 – Vamos baixar os dados do restaurante no component DetailsRestaurant/index.js usando nosso getRestaurant:
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 Details from './Details'; import CategoryProducts from './CategoryProducts'; import getRestaurant from '../../services/getRestaurant'; import { useRouter } from 'next/router'; import { Spinner, Alert } from 'react-bootstrap'; export default function DetailsRestaurant() { const router = useRouter(); const { id } = router.query; const { restaurant, isLoading, isError } = getRestaurant(id); if(isError) return <Alert variant='custom-red'>Erro ao carregar</Alert> else if(isLoading) return <Spinner animation='border'/> return( <> <Details {...restaurant} /> {restaurant.product_categories.map((product_category, i) => <CategoryProducts restaurant={restaurant} {...product_category} key={i} /> )} </> ) } |
9 – No component Details criado, coloque:
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 |
import { Row, Col, Card } from 'react-bootstrap'; import Image from 'next/image' import { FaStar } from 'react-icons/fa'; import toCurrency from '../../../services/toCurrency'; export default function Details(props) { return ( <> <h3 className='fw-bold'>{props.name}</h3> <Card className="mt-2 mb-4"> <Row className="my-3 mx-1"> <Col md={3} > <Image src={props.image_url} alt={props.name} width={300} height={200} layout="responsive" /> </Col> <Col md={9}> <p><small>{props.description}</small></p> <Row className='row-cols-auto'> <Col className="pr-0"> <small className='border px-3 border-custom-gray fw-bold'> entrega {toCurrency(props.delivery_tax)} </small> </Col> <Col > <small className='fw-bold'>{props.category_title}</small> </Col> <Col > <span className='text-custom-orange'> <FaStar/> 5 </span> </Col> </Row> </Col> </Row> </Card> </> ) } |
10 – No component CategoryProducts coloque:
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 |
import { Row, Col, Card } from 'react-bootstrap'; import Image from 'next/image' import toCurrency from '../../../services/toCurrency'; import truncateString from '../../../services/truncateString'; export default function CategoryProducts(props) { return( <> <h5 className='fw-bold'>{props.title}</h5> <Row> {props.products.map((product, i) => <Col md={4} sm={12} key={i}> <Card className="mb-4 clickable_effect"> <Row className="my-3 mx-1"> <Col md={6} xs={{span: 12, order: 2 }}> <p className='fw-bold mb-0'>{product.name}</p> <p><small>{truncateString(product.description, 80)}</small></p> <small className='border px-3 border-custom-gray fw-bold'> {toCurrency(product.price)} </small> </Col> <Col md={6} xs={{span: 12, order: 1 }} > <Image src={product.image_url} alt={product.name} width={300} height={200} layout="responsive" /> </Col> </Row> </Card> </Col> )} </Row> </> ) } |
O que é o Recoil?
O Recoil é uma biblioteca criada dentro do Facebook (assim como o próprio React) que tem como objetivo te ajudar a gerenciar estados de uma forma fácil e intuitiva (usar o recoil é como usar um hook qualquer no React).
Entendendo o Recoil
Na versão atual o Recoil é baseado principalmente em dois pilares, os Atoms e os Selectors.
Atoms
Atoms são unidades de estado, é possível atualizar e ler estes estados de uma forma fácil. Também é possível conectar seus components a estes Atoms para que quando eles sejam atualizados os components sejam renderizados novamente. Você também pode usar os Atoms ao invés dos estados locais dos components e utiliza-los para compartilhar os estados entre muitos components.
Selectors
Os Selectors sâo funções puras (que tem como objetivo devolver valores derivados dos Atoms) que recebem um Atom como argumento, quando o Atom que veio como argumento é atualizado o selector também atualiza o valor de retorno. Assim como no caso dos Atoms, os Components também podem se ‘inscrever’ para serem avisados quando os selectors forem atualizados, quando isso acontece eles são renderizados novamente.
1 – Instale o recoil + recoil persist rodando:
1 |
yarn add recoil recoil-persist |
2 – No arquivo _app.js:
a – importe o recoil:
1 |
import { RecoilRoot } from 'recoil'; |
b – Em volta de <Component {…pageProps} /> coloque:
1 2 3 |
<RecoilRoot> <Component {...pageProps} /> </RecoilRoot> |
c – Crie as seguintes pastas:
1 2 3 |
mkdir store mkdir store/atoms touch store/atoms/addressAtom.js |
d – No atom address criado coloque:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import { atom } from 'recoil'; import { recoilPersist } from 'recoil-persist' const { persistAtom } = recoilPersist() const addressState = atom({ key: 'addressState', default: { city: "", neighborhood: "", street: "", number: "", complement: "" }, effects_UNSTABLE: [persistAtom] }); export default addressState; |
1 – Instale o friendly id na API e configure ele nos Restaurantes
2 – Devolva o slug do restaurante junto com os outros detalhes dele em /restaurants e /restaurants/:id
3 – Altere o cliente web para usar o friendly id na url e na chamada da API