Aula 3: Conclusão do APP
Formulário de Endereço: 00:01:03:11
Modal para Incluir no Carrinho: 00:37:46:06
Desafio Adicionar Produto Repetido: 00:58:40:07
Criando o Carrinho: 00:59:48:02
Finalização do Pedido: 01:20:43:27
Página de Sucesso: 01:42:50:12
Desafio Página 404: 01:48:38:21
SSR, SSG e Production: 01:49:23:26
[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: dating frenzy games2win
- API completa no GitHub: https://onebitcode.com/scorpio-man-dating/
- Cliente Web no GitHub: 18-25 dating
Nesta parte vamos criar a base do Formulário de endereço que é onde o usuário vai selecionar o endereço de busca de restaurantes e de entrega dos produtos.
1 – Vamos atualizar a API para que ela devolva as cidades disponíveis no padrão adequado, na API em views/available_cities/index.json.jbuilder coloque:
1 |
json.array! @available_cities |
2 – Vamos atualizar o _app para que o RecoilRoot também envolva o Header (que vai usar estados dele):
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 |
import Head from 'next/head'; import '../styles/globals.scss' import Header from '../components/Header'; import Container from 'react-bootstrap/Container'; import { RecoilRoot } from 'recoil'; function MyApp({ Component, pageProps }) { return ( <> <Head> <title>OneBitFood V2</title> <link ref="icon" href="/favicon.icon" /> </Head> <main> <RecoilRoot> <Header /> <Container className='mt-6'> <Component {...pageProps} /> </Container> </RecoilRoot> </main> </> ) } export default MyApp |
3 – Crie o componente para o form de endereço:
1 2 3 4 |
mkdir components/AddressModal touch components/AddressModal/index.js mkdir components/AddressModal/FormAddress touch components/AddressModal/FormAddress/index.js |
4 – Para criar o modal de endereço coloque o seguinte código em components/AddressModal/index.js`:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import Modal from 'react-bootstrap/Modal'; export default function AddressModal(props) { return ( <Modal show={props.show} size="sm" aria-labelledby="contained-modal-title-vcenter" centered backdrop="static" keyboard={false} > <Modal.Header> <h5 className='fw-bold mt-2'>Endereço de entrega</h5> </Modal.Header> <Modal.Body> Hello </Modal.Body> </Modal> ) } |
5 – Para incluir a chamada do modal no Header, faça as seguintes inclusões:
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 |
import { useState } from 'react'; import { Navbar, Nav } from 'react-bootstrap'; ... import AddressModal from '../AddressModal'; import { FaCrosshairs } from 'react-icons/fa'; export default function Header() { const [addressModalShow, setAddressModalShow] = useState(false); return ( ... <Navbar.Collapse id='responsive-navbar-nav' className='justify-content-end'> <Nav className="py-2 text-center"> <span className="clickable_effect" onClick={() => setAddressModalShow(true)}> <FaCrosshairs className='mb-1'/> Endereço </span> <AddressModal show={addressModalShow} onHide={() => setAddressModalShow(false)} onShow={() => setAddressModalShow(true)} /> </Nav> <SearchBox/> </Navbar.Collapse> </Navbar> ) } |
6 – Crie um service chamado getAvailableCities.js e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 12 |
import useSWR from 'swr'; export default function getAvailableCities() { const fetcher = (...args) => fetch(...args).then((res) => res.json()); const { data, error } = useSWR( `${process.env.apiUrl}/api/available_cities`, fetcher, { revalidateOnFocus: false } ) console.log(data) return { available_cities: data, isLoading: !error && !data, isError: error } } |
7 – Agora, no component FormAddress 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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
import { useState, useEffect } from 'react'; import { Row, Col, Button, Form, Spinner, Alert } from 'react-bootstrap'; import { useRouter } from 'next/router' import { useRecoilState } from 'recoil'; import addressState from '../../../store/atoms/addressAtom'; import getAvailableCities from '../../../services/getAvailableCities'; export default function FormAddress(props) { const { available_cities, isLoading, isError } = getAvailableCities(); const [address, setAddress] = useRecoilState(addressState); const [cityChanged, setCityChanged] = useState(false); const router = useRouter(); if(isError) return <Alert variant='custom-red'>Erro ao carregar</Alert> else if(isLoading) return <Spinner animation='border'/> const updateAddress = (e) => { if(e.target.name == 'city') setCityChanged(true); setAddress({ ...address, [e.target.name]: e.target.value }); } const confirmAddress = (e) => { e.preventDefault(); props.onHide(); if(cityChanged) router.push('/restaurants'); } return ( <Row> <Col md={12}> <Form onSubmit={e => confirmAddress(e)}> <Form.Group> <Form.Label>Sua cidade</Form.Label> <Form.Control required as="select" onChange={updateAddress} value={address.city} name="city" > {address.city == '' && <option key={0}>Escolher cidade</option>} {available_cities.map((state, i) => { return <option key={i} value={state}>{state}</option> })} </Form.Control> </Form.Group> {address.city != '' && <div> <Form.Group className='mt-3'> <Form.Label>Bairro</Form.Label> <Form.Control required type="text" placeholder="Bairro" onChange={updateAddress} value={address.neighborhood} name="neighborhood" /> </Form.Group> <Form.Group className='mt-3'> <Form.Label>Logradouro</Form.Label> <Form.Control required type="text" placeholder="Rua/Avenida/Alameda" onChange={updateAddress} value={address.street} name="street" /> </Form.Group> <Form.Group className='mt-3'> <Form.Label>Nûmero</Form.Label> <Form.Control required type="text" placeholder="Nûmero" onChange={updateAddress} value={address.number} name="number" /> </Form.Group> <Form.Group className='mt-3'> <Form.Label>Complemento</Form.Label> <Form.Control type="text" placeholder="Complemento" onChange={updateAddress} value={address.complement} name="complement" /> </Form.Group> <div className="text-center pt-4"> <Button variant="custom-red" className='text-white' type="submit" size="md"> Confirmar endereço </Button> </div> </div> } </Form> </Col> </Row> ) } |
8 – Inclua ele no AddressModal inserindo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... import FormAddress from './FormAddress' export default function AddressModal(props) { return ( ... <Modal.Body> <FormAddress onHide={() => props.onHide()} onShow={() => props.onShow()} /> </Modal.Body> </Modal> ) } |
9 – Ainda no AddressModal agora vamos exibir o modal de endereço sempre que ele não tenha sido preenchido ainda (exceto na página inicial):
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 |
import { useEffect } from 'react'; import Modal from 'react-bootstrap/Modal'; import FormAddress from './FormAddress'; import { useRecoilState } from 'recoil'; import addressState from '../../store/atoms/addressAtom'; import { useRouter } from 'next/router'; export default function AddressModal(props) { const [address, setAddress] = useRecoilState(addressState) const router = useRouter(); useEffect(() => { if(router.asPath != '/' && address.city == '') props.onShow(); }, [router]) return( <Modal show={props.show} size='sm' aria-labelledby='contained-modal-title-vcenter' centered backdrop='static' keybord={false} > <Modal.Header> <h5 className='fw-bold mt-2'>Endereço de entrega</h5> </Modal.Header> <Modal.Body> <FormAddress onHide={() => props.onHide()} /> </Modal.Body> </Modal> ) } |
10 – Para finalizar, atualize o getRestaurants para fazer o filtro por cidade:
1 2 3 4 5 6 7 8 9 10 11 12 |
... import { useRecoilState } from 'recoil'; import addressState from '../store/atoms/addressAtom'; export default function getRestaurants() { const [address, setAddress] = useRecoilState(addressState); ... if(address.city != '') params = `${params == '' ? '?' : `${params}&`}city=${address.city}` ... } |
1 – Crie o Atom cartAtom.js dentro de store/atoms e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 12 |
import { atom } from 'recoil'; import { recoilPersist } from 'recoil-persist' const { persistAtom } = recoilPersist() const cartState = atom({ key: 'cartState', default: {restaurant: {}, products: []}, effects_UNSTABLE: [persistAtom] }); export default cartState; |
2 – Crie o component AddProductModal rodando:
1 2 |
mkdir components/AddProductModal touch components/AddProductModal/index.js |
3 – Inclua a estrutura principal do component no arquivo criado:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import { Modal } from 'react-bootstrap'; export default function AddProductModal(props) { return ( <Modal show={props.show} size="sm" aria-labelledby="contained-modal-title-vcenter" centered keyboard={false} onHide={() => props.onHide()} > <Modal.Header closeButton> <h5 className='fw-bold mt-2'>Adicionar produto</h5> </Modal.Header> <Modal.Body> Hello </Modal.Body> </Modal> ) } |
4 – Inclua ele no component CategoryProducts do DetailsRestaurant:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import { useState } from 'react'; ... import AddProductModal from '../../AddProductModal'; export default function CategoryProducts(props) { const [productSelected, setProductSelected] = useState(null); return( <> <AddProductModal show={productSelected != null} onHide={() => setProductSelected(null)} product={productSelected} restaurant={props.restaurant} /> <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" onClick={() => setProductSelected(product)}> ... </> ) } |
5 – No AddModalProduct coloque a lógica:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 |
import { useState } from 'react'; import { Modal, Row, Col, Form, Button } from 'react-bootstrap'; import Image from 'next/image' import toCurrency from '../../services/toCurrency'; import truncateString from '../../services/truncateString'; import { useRecoilState } from 'recoil'; import cartState from '../../store/atoms/cartAtom'; export default function AddProductModal(props) { const [quantity, setQuantity] = useState(1); const [cart, setCart] = useRecoilState(cartState); const addProduct = (e) => { e.preventDefault(); const product = {...props.product, ...{'quantity': quantity}} if(cart.restaurant.id != props.restaurant.id) setCart({ restaurant: props.restaurant, products: [product] }) else setCart({ restaurant: props.restaurant, products: [...cart.products, product] }) setQuantity(1); props.onHide(); } if (!props.product) return null; return ( <Modal show={props.show} size="sm" aria-labelledby="contained-modal-title-vcenter" centered keyboard={false} onHide={() => props.onHide()} > <Modal.Header> <h5 className='fw-bold mt-2'>Adicionar produto</h5> </Modal.Header> <Modal.Body> <Row> <Col> <Image src={props.product.image_url} alt={props.product.name} width={300} height={200} /> </Col> </Row> <Row className="pb-0"> <Col md={8}> <p className='fw-bold mb-0'>{props.product.name}</p> </Col> <Col> <small className='border px-1 border-custom-gray fw-bold'> {toCurrency(props.product.price)} </small> </Col> </Row> <Row> <Col> <p><small>{truncateString(props.product.description, 60)}</small></p> </Col> </Row> <Form onSubmit={addProduct} className='d-flex'> <Form.Group> <Form.Control required type="number" placeholder="quantidade" min="1" step="1" name="quantidade" value={quantity} onChange={(e) => setQuantity(e.target.value)} /> </Form.Group> <Button variant="custom-red" type="submit" className="text-white ms-6"> Adicionar </Button> </Form> </Modal.Body> </Modal> ) } |
6 – No component CategoryProducts na primeira coluna aumente o tamanho dela (md={4}) para 6 (md={6})
7 – Teste subindo o projeto (e observando o localstorage no application das ferramentas de depuração do browser):
1 |
yarn dev |
No Modal de adicionar produto, na hora da adição verifique se o produto já foi previamente adicionado ao carrinho, se sim, ao invés de adiciona-lo novamente aumente a sua quantidade no pedido.
1 – Gere os components necessários para o carrinho rodando:
1 2 3 4 |
mkdir components/Cart touch components/Cart/index.js mkdir components/CartModal touch components/CartModal/index.js |
2 – No component Cart criei a estrutura do modal:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import Modal from 'react-bootstrap/Modal'; export default function CartModal(props) { return ( <Modal show={props.show} size="sm" aria-labelledby="contained-modal-title-vcenter" centered keyboard={false} onHide={() => props.onHide()} > <Modal.Header> <h5 className='fw-bold mt-2'>Carrinho</h5> </Modal.Header> <Modal.Body> Hello </Modal.Body> </Modal> ) } |
3 – Adicione a chamada dele no Header:
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 |
... import CartModal from '../CartModal'; import { FaCrosshairs, FaShoppingBag } from 'react-icons/fa'; export default function Header() { ... const [cartModalShow, setCartModalShow] = useState(false); return ( <Navbar bg='white' expand='lg' className='border-bottom border-custom-gray'> <Navbar.Brand className='mx-3'> ... </Navbar.Brand> <Navbar.Toggle aria-controls='responsive-navbar-nav' /> <Navbar.Collapse id='responsive-navbar-nav' className='justify-content-end'> <Nav className="me-lg-4 me-sm-0 text-center pt-2 pb-2"> <span className="clickable_effect" onClick={() => setCartModalShow(true)}> <FaShoppingBag/> Carrinho </span> <CartModal show={cartModalShow} onHide={() => setCartModalShow(false)} onShow={() => setCartModalShow(true)} /> </Nav> <Nav className="py-2 text-center"> ... </Nav> <SearchBox/> </Navbar.Collapse> </Navbar> ) } |
4 – No component Cart inclua a estrutura do component:
1 2 3 4 5 6 7 8 |
export default function Cart(props) { return ( <> </> ) } |
5 – Nesse mesmo component: a – Inclua o atom cart:
1 2 3 4 5 6 7 8 9 |
... import { useRecoilState } from 'recoil'; import cartState from '../../store/atoms/cartAtom'; export default function Cart() { const [cart, setCart] = useRecoilState(cartState); ... |
b – Inclua os components bootstrap e os services de formatação:
1 2 3 |
import { Row, Col, Button } from 'react-bootstrap'; import toCurrency from '../../services/toCurrency'; import truncateString from '../../services/truncateString'; |
c – Coloque o métodos para calcular o subtotal e o total do carrinho e a lógica para avisar quando o carrinho está vázio:
1 2 3 4 5 6 7 8 9 10 11 |
export default function Cart() { ... const subTotal = () => cart.products.reduce( (a, b) => a + (parseFloat(b['price']) * parseFloat(b['quantity']) || 0), 0 ); const total = () => cart.restaurant.delivery_tax + subTotal(); if (cart.products.length <= 0) return <p>Carrinho vazio</p>; |
d – Inclua o método para remover produtos do carrinho:
1 2 3 4 5 6 7 |
export default function Cart() { ... const removeProduct = (product) => { const new_products = cart.products.filter((p) => p.id != product.id); setCart({ restaurant: { ...cart.restaurant }, products: new_products }); } |
e – Agora inclua a parte visual:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
... return ( <> <h5 className='fw-bolder'>{cart.restaurant.name}</h5> <hr /> {cart.products.map((product, i) => <div key={product.id} className="mb-4" key={i}> <Row> <Col md={8} xs={8}> <small className='fw-bolder'>{product.quantity}x {product.name}</small> </Col> <Col md={4} xs={4} className="text-right"> <small > {toCurrency(product.price)} </small> </Col> </Row> <Row className="mt-2"> <Col md={8} xs={8}> <p><small>{truncateString(product.description, 40)}</small></p> </Col> <Col md={4} xs={4} className="text-right"> <Button size="sm" variant="outline-dark" onClick={() => removeProduct(product)} className='border px-1 border-custom-gray' > Remover </Button> </Col> </Row> </div> )} <hr /> <Row className="mt-4"> <Col md={8} xs={8}> <p>Subototal</p> </Col> <Col md={4} xs={4} className="text-right"> <p>{toCurrency(subTotal())}</p> </Col> </Row> <Row className="mt-n2"> <Col md={8} xs={8}> <p>Taxa de entrega</p> </Col> <Col md={4} xs={4} className="text-right"> <p>{toCurrency(cart.restaurant.delivery_tax)}</p> </Col> <hr /> </Row> <Row className="mb-4"> <Col md={8} xs={8}> <p className='fw-bolder'>Total</p> </Col> <Col md={4} xs={4} className="text-right"> <p className='fw-bolder'>{toCurrency(total())}</p> </Col> </Row> </> ) ... |
6 – No component CartModal: a – Inclua o Cart:
1 2 3 4 5 6 7 8 |
... import Cart from '../Cart'; ... <Modal.Body> <Cart show={props.show} /> </Modal.Body> ... |
b – Inclua o atom cart também:
1 2 3 4 5 6 |
... import { useRecoilState } from 'recoil'; import cartState from '../../store/atoms/cartAtom'; export default function CartModal(props) { const [cart] = useRecoilState(cartState); |
c – Para podermos ir para a página de finalização inclua:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
... import Link from 'next/link'; import Button from 'react-bootstrap/Button'; ... <Modal.Body> <Cart show={props.show} /> {cart.products.length > 0 && <div className="text-center pt-2"> <Link href='/orders/new'> <Button variant="custom-red text-white">Finalizar pedido</Button> </Link> </div> } </Modal.Body> ... |
7 – Suba o projeto e veja como ficou:
1 |
yarn dev |
Para referência: 1 – CartModal completo:
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 |
import Modal from 'react-bootstrap/Modal'; import Cart from '../Cart'; import { useRecoilState } from 'recoil'; import cartState from '../../store/atoms/cartAtom'; import Link from 'next/link'; import Button from 'react-bootstrap/Button'; export default function CartModal(props) { const [cart] = useRecoilState(cartState); return ( <Modal show={props.show} size="sm" aria-labelledby="contained-modal-title-vcenter" centered keyboard={false} onHide={() => props.onHide()} > <Modal.Header> <h5 className='fw-bold mt-2'>Carrinho</h5> </Modal.Header> <Modal.Body> <Cart show={props.show} /> {cart.products.length > 0 && <div className="text-center pt-2"> <Link href='/orders/new'> <Button variant="custom-red text-white" onClick={props.onHide}> Finalizar pedido </Button> </Link> </div> } </Modal.Body> </Modal> ) } |
2 – Cart completo:
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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 |
import React from 'react'; import { useRecoilState } from 'recoil'; import cartState from '../../store/atoms/cartAtom'; import { Row, Col, Button } from 'react-bootstrap'; import toCurrency from '../../services/toCurrency'; import truncateString from '../../services/truncateString'; export default function Cart() { const [cart, setCart] = useRecoilState(cartState); const removeProduct = (product) => { const new_products = cart.products.filter((p) => p.id != product.id); setCart({ restaurant: { ...cart.restaurant }, products: new_products }); } const subTotal = () => cart.products.reduce( (a, b) => a + (parseFloat(b['price']) * parseFloat(b['quantity']) || 0), 0 ); const total = () => cart.restaurant.delivery_tax + subTotal(); if (cart.products.length <= 0) return <p>Carrinho vazio</p>; return ( <> <h5 className='fw-bolder'>{cart.restaurant.name}</h5> <hr /> {cart.products.map((product, i) => <div key={product.id} className="mb-4" key={i}> <Row> <Col md={8} xs={8}> <small className='fw-bolder'>{product.quantity}x {product.name}</small> </Col> <Col md={4} xs={4} className="text-right"> <small > {toCurrency(product.price)} </small> </Col> </Row> <Row className="mt-2"> <Col md={8} xs={8}> <p><small>{truncateString(product.description, 40)}</small></p> </Col> <Col md={4} xs={4} className="text-right"> <Button size="sm" variant="outline-dark" onClick={() => removeProduct(product)} className='border px-1 border-custom-gray' > Remover </Button> </Col> </Row> </div> )} <hr /> <Row className="mt-4"> <Col md={8} xs={8}> <p>Subototal</p> </Col> <Col md={4} xs={4} className="text-right"> <p>{toCurrency(subTotal())}</p> </Col> </Row> <Row className="mt-n2"> <Col md={8} xs={8}> <p>Taxa de entrega</p> </Col> <Col md={4} xs={4} className="text-right"> <p>{toCurrency(cart.restaurant.delivery_tax)}</p> </Col> <hr /> </Row> <Row className="mb-4"> <Col md={8} xs={8}> <p className='fw-bolder'>Total</p> </Col> <Col md={4} xs={4} className="text-right"> <p className='fw-bolder'>{toCurrency(total())}</p> </Col> </Row> </> ) } |
3 – Header completo:
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 50 51 52 53 54 55 56 57 58 |
import { useState } from 'react'; import { Navbar, Nav } from 'react-bootstrap'; import Image from 'next/image'; import Link from 'next/link'; import SearchBox from '../SearchBox'; import AddressModal from '../AddressModal'; import CartModal from '../CartModal'; import { FaCrosshairs, FaShoppingBag } from 'react-icons/fa'; export default function Header() { const [addressModalShow, setAddressModalShow] = useState(false); const [cartModalShow, setCartModalShow] = useState(false); 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'> <Nav className="me-lg-4 me-sm-0 text-center pt-2 pb-2"> <span className="clickable_effect" onClick={() => setCartModalShow(true)}> <FaShoppingBag/> Carrinho </span> <CartModal show={cartModalShow} onHide={() => setCartModalShow(false)} onShow={() => setCartModalShow(true)} /> </Nav> <Nav className="py-2 text-center"> <span className="clickable_effect" onClick={() => setAddressModalShow(true)}> <FaCrosshairs className='mb-1'/> Endereço </span> <AddressModal show={addressModalShow} onHide={() => setAddressModalShow(false)} onShow={() => setAddressModalShow(true)} /> </Nav> <SearchBox/> </Navbar.Collapse> </Navbar> ) } |
1 – Crie a page Orders:
1 2 |
mkdir pages/orders touch pages/orders/new.js |
2 – Crie os components base:
1 2 3 4 |
mkdir components/NewOrder touch components/NewOrder/index.js mkdir components/NewOrder/OrderForm touch components/NewOrder/OrderForm/index.js |
3 – Crie a base dos components: a – NewOrder:
1 2 3 4 5 6 |
export default function NewOrder() { return ( <> </> ) } |
b – OrderForm:
1 2 3 4 5 6 |
export default function OrderForm() { return ( <> </> ) } |
4 – Coloque na página criada:
1 2 3 4 5 |
import NewOrder from "../../components/NewOrder"; export default function New() { return <NewOrder /> } |
5 – Vamos criar a base do component NewOrder:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import React from 'react'; import { Row, Col, Card } from 'react-bootstrap'; export default function NewOrder() { return ( <> <Row className='justify-content-md-center'> <Col md={{span: 4}}> <Card className='p-5 mb-4'> <Cart/> </Card> </Col> <Col md={{span: 4}}> <Card className='p-5 mb-4'> <OrderForm/> </Card> </Col> </Row> </> ) } |
6 – Vamos importar e adicionar o Cart nele:
1 2 3 4 5 6 7 8 9 10 |
... import Cart from '../Cart'; ... <Row> <Col md={{span: 4, offset: 1}}> <Card className='p-5 mb-4'> <Cart/> </Card> ... |
7 – Vamos criar um service para enviar a order para o backend, crie um service chamado createOrder.js e coloque nele:
1 2 3 4 5 6 7 8 9 10 11 |
export default async function createOrder(order) { const response = await fetch(`${process.env.apiUrl}/api/orders`, { method: 'POST', headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' }, body: JSON.stringify({order: order}), }); return response; } |
8 – No component OrderForm: a – Importe o service criado:
1 |
import createOrder from '../../../services/createOrder'; |
b – Inclua também os atoms cart e address:
1 2 3 4 5 6 7 8 9 10 11 |
import { useRecoilState, useResetRecoilState } from 'recoil'; import addressState from '../../../store/atoms/addressAtom'; import cartState from '../../../store/atoms/cartAtom'; ... export default function OrderForm() { const [address] = useRecoilState(addressState); const [cart] = useRecoilState(cartState); const resetCart = useResetRecoilState(cartState); ... |
c – Crie um state para mostrar uma mensagem de erro quando a chamada falhar:
1 2 3 4 5 6 |
import { useState } from 'react'; ... export default function OrderForm() { const [error, setError] = useState(null) ... |
d – Importe também o router:
1 2 3 4 5 6 7 |
import { useRouter } from 'next/router'; ... export default function OrderForm() { ... const router = useRouter(); ... |
e – Importe os components do Bootstrap que vamos usar:
1 |
import { Form, Alert, Button } from 'react-bootstrap'; |
f – Crie um state order:
1 2 3 4 5 6 7 8 9 10 11 |
export default function OrderForm() { ... const [order, setOrder] = useState({ name: "", phone_number: "", ...address, order_products_attributes: cart.products.map(p => ( {'product_id': p.id, 'quantity': p.quantity} )), restaurant_id: cart.restaurant.id }) |
g – Inclua um método para atualizar o estado order:
1 2 3 |
const updateOrderState = (e) => { setOrder({ ...order, [e.target.name]: e.target.value }); } |
h – Agora crie um método para chamar nosso serviço de criação de order:
1 2 3 4 5 6 7 8 9 10 |
const submitOrder = async (e) => { e.preventDefault(); try { await createOrder(order); router.push('/orders/success'); resetCart(); } catch(err) { setError(true); } } |
i – Por fim inclua a parte visual do component:
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 |
return ( <Form onSubmit={e => submitOrder(e)}> <h4 className='fw-bold mb-5'>Detalhes finais</h4> <Form.Group> <Form.Label>Nome completo</Form.Label> <Form.Control required type="text" placeholder="Dennis Ritchie..." onChange={updateOrderState} value={order.name} name="name" /> </Form.Group> <Form.Group className='mt-3'> <Form.Label>Número de telefone</Form.Label> <Form.Control required type="text" placeholder="(00) 00000-0000" onChange={updateOrderState} value={order.phone_number} name="phone_number" /> </Form.Group> <div className="mt-5"> <p className='fw-bolder'>Entregar em:</p> <p><small>{address.street} {address.number} {address.neighborhood}, {address.city}</small></p> </div> {cart.products.length > 0 && <div className="text-center"> <Button variant="custom-red" type="submit" size="lg" className="mt-4 text-white"> Finalizar Pedido </Button> </div> } {error && <Alert variant='custom-red' className="mt-4"> Erro no pedido! </Alert> } </Form> ) |
8 – Atualize o component OrderNew para incluir o component criado anteriormente:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
import React from 'react'; import { Row, Col, Card } from 'react-bootstrap'; import OrderForm from './OrderForm'; import Cart from '../Cart'; export default function NewOrder() { return ( <> <Row> <Col md={{span: 4, offset: 1}}> ... </Col> <Col md={{span: 4, offset: 2}}> <Card className='p-5 mb-4'> <OrderForm/> </Card> </Col> </Row> </> ) } |
9 – Teste finalizar um pedido:
1 |
yarn dev |
1 – Dentro de pages/orders crie a página success.js e coloque nela:
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 |
import { Row, Col, Card } from 'react-bootstrap'; import Image from 'next/image' export default function Success() { return ( <Row className="mt-4 justify-content-md-center"> <Col md={{ span: 4 }}> <Card className="mt-4 px-4 py-4"> <h4 className='fw-bold'>Pedido a caminho</h4> <p className="mt-2">Em breve você receberá sua comida em casa!</p> <Row className="my-4 justify-content-md-center"> <Col md={{ span: 10 }}> <Image src='/status-ok.png' alt='Sucesso no pedido' width={100} height={100} layout="responsive" /> </Col> </Row> </Card> </Col> </Row> ) } |
2 – Suba o projeto e teste todo o processo de escolha, seleção do produto e realização do pedido:
1 |
yarn dev |
Renderização do lado do cliente (CSR)
Prós
-
Rápido no servidor – Como você está apenas renderizando uma página em branco, é muito rápido renderizar.
-
Pode ser estática – a página em branco pode ser gerada estaticamente e servida a partir de algo como S3, o que a torna ainda mais rápida.
-
Oferece suporte a aplicativos de página única – a renderização do lado do cliente é o único modelo que oferece suporte a aplicativos de página única ou SPAs.
Contras
Sem renderização inicial – você está enviando uma página em branco para o cliente. Portanto, se seu aplicativo for grande ou o cliente estiver em uma conexão lenta, isso não será o ideal.
Renderização do lado do servidor (SSR)
Prós
-
Conteúdo imediatamente disponível – como você está enviando HTML renderizado para o cliente, o cliente começará a ver o conteúdo quase imediatamente.
-
Nenhuma busca adicional do cliente – Idealmente, o processo de renderização do servidor fará todas as chamadas necessárias para obter os dados, portanto, você não fará nenhuma chamada de serviço adicional do cliente. Pelo menos até que o usuário comece a brincar um pouco com a página.
-
Ótimo para SEO
Contras
-
Mais lento no servidor – você está renderizando a página duas vezes, uma no servidor e outra no cliente. Você provavelmente também está fazendo chamadas de serviço do servidor para processar a página. Tudo isso leva tempo, então o envio inicial do HTML ao cliente pode ser atrasado.
-
Incompatível com algumas bibliotecas de IU
Geração de site estático (SSG)
Prós
-
Conteúdo imediatamente disponível – como você está enviando HTML renderizado para o cliente, o cliente começará a ver o conteúdo quase imediatamente.
-
Nenhuma busca adicional do cliente – Idealmente, o processo de renderização do servidor fará todas as chamadas necessárias para obter os dados, portanto, você não fará nenhuma chamada de serviço adicional do cliente. Pelo menos até que o usuário comece a brincar um pouco com a página.
-
Ótimo para SEO – motores de busca como conteúdo HTML. Se você está usando apenas a renderização do lado do cliente, então está contando com os motores de busca rodando seu Javascript, o que pode ou não acontecer.
-
Rápido de servir incrível – O conteúdo estático é muito rápido de servir. Você também pode armazená-lo em cache de forma limpa.
-
Sem servidor – você não precisa executar um servidor. Portanto, você não precisa monitorar esse servidor e obterá muito menos pings de dever de pager.
Contras
-
Pode ser lento para reconstruir sites grandes – Se você tem dezenas de milhares de rotas, deve esperar lentidão. Embora a equipe do NextJS esteja trabalhando em reconstruções incrementais.
-
Incompatível com algumas bibliotecas de IU
Usando SSR no projeto
1 – Atualize a page restaurants/[id].js colocando: a – A função getServerSideProps para baixar as informações do servidor assim que a página for chamada:
1 2 3 4 5 6 7 8 9 10 11 |
export async function getServerSideProps(context) { const { id } = context.query; try { const res = await fetch(`${process.env.apiUrl}/api/restaurants/${id}`); const restaurant = await res.json(); const isError = res.ok ? false : true return { props: { restaurant, isError: isError }, } } catch { return { props: { isError: true }, } } } |
b – Atualize a função Restaurant:
1 2 3 |
export default function Restaurant({restaurant, isError = false}) { return <DetailsRestaurant restaurant={restaurant} isError={isError}/> } |
2 – Atualize o component DetailsRestaurant colocando:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
import Details from './Details'; import CategoryProducts from './CategoryProducts'; import { Alert } from 'react-bootstrap'; export default function DetailsRestaurant(props) { if(props.isError) return <Alert variant='custom-red'>Erro ao carregar</Alert> return( <> <Details {...props.restaurant} /> {props.restaurant.product_categories.map((product_category, i) => <CategoryProducts restaurant={props.restaurant} {...product_category} key={i} /> )} </> ) } |
3 – Suba o projeto para ver como ficou:
1 |
yarn dev |
Usando SSG
1 – Atualize a page restaurants/[id].js colocando: a – A função getStaticPaths para pegar todos os ids dos restaurantes:
1 2 3 4 5 6 7 8 9 10 |
export async function getStaticPaths() { const res = await fetch(`${process.env.apiUrl}/api/restaurants`); const restaurants = await res.json(); const paths = restaurants.map((restaurant) => ({ params: { id: restaurant.id.toString() } })) return { paths, fallback: false } } |
b – A função getStaticProps para baixar cada um dos restaurantes disponíveis:
1 2 3 4 5 6 7 8 |
export async function getStaticProps({ params }) { const res = await fetch(`${process.env.apiUrl}/api/restaurants/${params.id}`); const restaurant = await res.json(); return { props: { restaurant }, revalidate: 120 } } |
c – Atualize a função Restaurante colocando:
1 2 3 |
export default function Restaurant({restaurant}) { return <DetailsRestaurant restaurant={restaurant} /> } |
2 – Atualize o component DetailsRestaurant colocando:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
import Details from './Details'; import CategoryProducts from './CategoryProducts'; export default function DetailsRestaurant(props) { return( <> <Details {...props.restaurant} /> {props.restaurant.product_categories.map((product_category, i) => <CategoryProducts restaurant={props.restaurant} {...product_category} key={i} /> )} </> ) } |
Colocando o projeto em modo Production
1 – Gere a versão para production do seu projeto rodando:
1 |
yarn build |
2 – Em package.json atualize a linha “start”: “next start” para:
1 |
"start": "next start -p 3001" |
3 – Rode seu projeto como production:
1 |
yarn start |