(pos) Programação Orientada a Serviços

Página da disciplina de pos (Programação Orientada a Serviços) do curso técnico integrado de Informática para Internet.

Notas de aula
Conograma
Avaliação
Links
CADES :D

React App - Componentes complexos e domínio de aplicação

Objetivos

Componentes React/BeHappyWith.me

Sumário

  1. Preparando o ambiente
  2. Adicionar domínio da aplicação
  3. Criar e testar o componente Button
  4. Criar e testar o componente ImageScroller
  5. Registrar as mudanças no repositório do github
  6. Publicar no Heroku
  7. Homework

1. Preparando o ambiente

para quem não tem o pnpm, npm, node

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash # install nvm
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"  # This loads nvm
[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion"  # This loads nvm bash_completion
nvm install node # install node lastest version
npm i -g pnpm # instal pnpm

para quem tem o código no computador

### acessa diretório da aplicação
cd [nickname]/behappywith.me
git pull ### baixa o código-fonte

### Modifica informações do desenvolvedor
git config --global --unset user.name
git config --global --unset user.email
git config user.name "$NOME"
git config user.email "$EMAIL"

atom . ### abre o editor de código-fonte com o diretório atual

### Instala as bibliotecas do app
pnpm install
pnpm start

para quem NÃO tem o código no computador

### acessa diretório da aplicação
mkdir [nickname] && cd $_
### Mudar o [github-username] pelo seu nickname do GitHub
git clone https://github.com/[github-username]/behappy-frontend.git
mv behappy-frontend behappywith.me && cd $_

### Modifica informações do desenvolvedor
git config --global --unset user.name
git config --global --unset user.email
git config user.name "$NOME"
git config user.email "$EMAIL"

atom . ### abre o editor de código-fonte com o diretório atual

### Instala as bibliotecas do app
pnpm install
pnpm start

2. Adicionar o domínio da aplicação

Sub-tarefas

  1. Criar diretório para o domínio
  2. Criar e editar a classe Usuário do domínio
  3. Modificar o componente React/BeHappyWith.Me NewUser

2.1. Criar diretório para o domínio

mkdir src/models
touch src/models/User.js

2.2. Criar e editar a classe Usuário do domínio

src/models/User.js

export default class User {
  constructor() {
    this.name = "";
    this.gender = "";
  }

  validName = () => {
    return (
      typeof this.name === "string" &&
      this.name.length !== 0 &&
      this.name.length <= 40
    );
  };

  validGender = () => {
    return ["m", "f"].some(param => {
      return this.gender === param;
    });
  };
}

2.3. Modificar o componente React/BeHappyWith.Me NewUser

src/components/NewUser/NewUser.js

import React, { Component } from "react";

import Label from "../Label";
import Input from "../Input";
import GenderSelector from "../GenderSelector";
import User from "../../models/User";

class NewUser extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: new User(),
      validation: {
        invalidName: false,
        invalidGender: false
      }
    };
  }

  updateUserName = event => {
    let user = this.state.user;
    user.name = event.target.value;
    this.setState({
      user: user
    });
  };

  updateUserGender = (event, gender) => {
    event.preventDefault();
    let user = this.state.user;
    user.gender = gender;
    this.setState({
      user: user
    });
  };

  render() {
    return (
      <div className="center">
        <form className="pure-form pure-form-stacked">
          <Label
            htmlFor="name"
            text="Quem é você?"
            invalidValue={this.state.validation.invalidName}
          />
          <Input
            id="name"
            placeholder="Digite seu nome"
            maxLength="40"
            readOnly={false}
            invalidValue={this.state.validation.invalidName}
            defaultValue={this.state.user.name}
            onChange={this.updateUserName}
          />
          <Label
            text="Seu gênero:"
            invalidValue={this.state.validation.invalidGender}
          />
          <GenderSelector
            invalidValue={this.state.validation.invalidGender}
            gender={this.state.user.gender}
            updateGender={this.updateUserGender}
          />
        </form>
      </div>
    );
  }
}

export default NewUser;

3. Criar o componente Button

Sub-tarefas

  1. Criar e testar o componente Button;
  2. Validar os dados do usuário;
  3. Apresentar mensagens de erro;
  4. Finalizar a 1a tela de novo usuário.

3.1. Criar e testar o componente Button

  1. Criar o diretório para o componente React/BeHappyWith.Me Button
  2. Criar e editar o componente React/BeHappyWith.Me Button
  3. Modificar o componente React/BeHappyWith.Me NewUser
mkdir src/components/Button
touch src/components/Button/index.js
touch src/components/Button/Button.js

src/components/Button/index.js

import Button from "./Button";

export default Button;

src/components/Button/Button.js

import React from "react";

export default function Button(props) {
  const className = props.main
    ? "pure-button pure-button-primary"
    : "pure-button";
  const style = {
    boxSizing: "border-box",
    backgroundColor: props.main ? "#2c80b9" : "#e6e6e6",
    float: props.main ? "right" : "left",
    marginTop: "10px",
    width: "120px",
    height: "38px"
  };

  return (
    <button className={className} style={style} onClick={props.onClick}>
      {props.text}
    </button>
  );
}

src/components/NewUser/NewUser.js 1a Versão : testando o componente

import React, { Component } from "react";

import Label from "../Label";
import Input from "../Input";
import GenderSelector from "../GenderSelector";
import Button from "../Button";
import User from "../../models/User";

class NewUser extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: new User(),
      validation: {
        invalidName: false,
        invalidGender: false
      }
    };
  }

  updateUserName = event => {
    let user = this.state.user;
    user.name = event.target.value;
    this.setState({
      user: user
    });
  };

  updateUserGender = (event, gender) => {
    event.preventDefault();
    let user = this.state.user;
    user.gender = gender;
    this.setState({
      user: user
    });
  };

  valid = e => {
    e.preventDefault();
    console.log("O botão próximo foi clicado...");
  };

  render() {
    return (
      <div className="center">
        <form className="pure-form pure-form-stacked">
          <Label
            htmlFor="name"
            text="Quem é você?"
            invalidValue={this.state.validation.invalidName}
          />
          <Input
            id="name"
            placeholder="Digite seu nome"
            maxLength="40"
            readOnly={false}
            invalidValue={this.state.validation.invalidName}
            defaultValue={this.state.user.name}
            onChange={this.updateUserName}
          />
          <Label
            text="Seu gênero:"
            invalidValue={this.state.validation.invalidGender}
          />
          <GenderSelector
            invalidValue={this.state.validation.invalidGender}
            gender={this.state.user.gender}
            updateGender={this.updateUserGender}
          />
          <Button main text="Próximo" onClick={this.valid} />
        </form>
      </div>
    );
  }
}

export default NewUser;

3.2 Validar os dados do usuário

src/components/NewUser/NewUser.js 2a Versão : validando os dados do usuário

import React, { Component } from "react";

import Label from "../Label";
import Input from "../Input";
import GenderSelector from "../GenderSelector";
import Button from "../Button";
import User from "../../models/User";

class NewUser extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: new User(),
      validation: {
        invalidName: false,
        invalidGender: false
      }
    };
  }

  updateUserName = event => {
    let user = this.state.user;
    user.name = event.target.value;
    this.setState({
      user: user
    });
  };

  updateUserGender = (event, gender) => {
    event.preventDefault();
    let user = this.state.user;
    user.gender = gender;
    this.setState({
      user: user
    });
  };

  valid = e => {
    e.preventDefault();
    let user = this.state.user;
    let validation = this.state.validation;

    validation.invalidName = !user.validName();
    validation.invalidGender = !user.validGender();

    this.setState({
      validation: validation
    });
  };

  render() {
    return (
      <div className="center">
        <form className="pure-form pure-form-stacked">
          <Label
            htmlFor="name"
            text="Quem é você?"
            invalidValue={this.state.validation.invalidName}
          />
          <Input
            id="name"
            placeholder="Digite seu nome"
            maxLength="40"
            readOnly={false}
            invalidValue={this.state.validation.invalidName}
            defaultValue={this.state.user.name}
            onChange={this.updateUserName}
          />
          <Label
            text="Seu gênero:"
            invalidValue={this.state.validation.invalidGender}
          />
          <GenderSelector
            invalidValue={this.state.validation.invalidGender}
            gender={this.state.user.gender}
            updateGender={this.updateUserGender}
          />
          <Button main text="Próximo" onClick={this.valid} />
        </form>
      </div>
    );
  }
}

export default NewUser;

3.3 Apresentar mensagens de erro

  1. Instalar o component react-toastify
  2. Criar o componente React/BeHappyWith.Me de mensgagens (src/components/Toast)
  3. Modificar o src/App.js
  4. Modificar o src/components/NewUser/NewUser.js
# Parar o servidor web
# pressiona as teclas CTRL + C

mkdir src/components/Toast
touch src/components/Toast/index.js
touch src/components/Toast/Toast.js

# Instalar o componente react-toastify
pnpm install react-toastify @babel/runtime react-transition-group

# Reiniciar o servidor web
pnpm start

React Toastify

particularidades do render():

  1. A propriedade position indical em qual trecho da tela a mensagem será exibida;
  2. O Toast, por padrão, fecha automaticamente. A propriedade autoClose define o tempo para fechar mensagem em milesegundos;
  3. hideProgressBar permite esconder a barra de progresso;
  4. closeOnClick permite fechar a mensagem com um clique;
  5. pauseOnHover interrompe contagem do tempo para fechamento.

src/components/Toast/index.js

import Toast from "./Toast";

export default Toast;

src/components/Toast/Toast.js

import React, { Component } from "react";
import { ToastContainer, toast } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";

class Toast extends Component {
  success = message => {
    toast.success(message);
  };

  info = message => {
    toast.info(message);
  };

  error = message => {
    toast.error(message);
  };

  render() {
    return (
      <ToastContainer
        position="bottom-center"
        autoClose={5000}
        hideProgressBar={true}
        pauseOnHover
      />
    );
  }
}

export default Toast;

src/App.js adicionando o Toast

import React, { Component } from "react";
// import logo from './logo.svg';

import "./App.css";
import Header from "./components/Header";
import NewUser from "./components/NewUser";
import Toast from "./components/Toast";

class App extends Component {
  render() {
    return (
      <div>
        <Header />
        <NewUser error={msg => this.refs.toast.error(msg)} />
        <Toast ref="toast" />
      </div>
    );
  }
}

export default App;

src/components/NewUser/NewUser.js adicionando as mensagens

import React, { Component } from "react";

import Label from "../Label";
import Input from "../Input";
import GenderSelector from "../GenderSelector";
import Button from "../Button";
import User from "../../models/User";

class NewUser extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: new User(),
      validation: {
        invalidName: false,
        invalidGender: false
      },
      firstView: false
    };
  }

  updateUserName = event => {
    let user = this.state.user;
    user.name = event.target.value;
    this.setState({
      user: user
    });
  };

  updateUserGender = (event, gender) => {
    event.preventDefault();
    let user = this.state.user;
    user.gender = gender;
    this.setState({
      user: user
    });
  };

  valid = e => {
    e.preventDefault();
    let user = this.state.user;
    let validation = this.state.validation;

    validation.invalidName = !user.validName();
    validation.invalidGender = !user.validGender();

    let message = "";
    let firstView = false;

    if (validation.invalidName && validation.invalidGender) {
      message = "Por favor, informe seu nome e gênero!!!";
    } else if (validation.invalidName) {
      message = "Por favor, informe seu nome!!!";
    } else if (validation.invalidGender) {
      message = "Por favor, selecione seu gênero!!!";
    } else {
      firstView = true;
    }

    if (!firstView) {
      this.props.error(message);
    }

    this.setState({
      validation: validation,
      firstView: firstView
    });
  };

  render() {
    return (
      <div className="center">
        <form className="pure-form pure-form-stacked">
          <Label
            htmlFor="name"
            text="Quem é você?"
            invalidValue={this.state.validation.invalidName}
          />
          <Input
            id="name"
            placeholder="Digite seu nome"
            maxLength="40"
            readOnly={false}
            invalidValue={this.state.validation.invalidName}
            defaultValue={this.state.user.name}
            onChange={this.updateUserName}
          />
          <Label
            text="Seu gênero:"
            invalidValue={this.state.validation.invalidGender}
          />
          <GenderSelector
            invalidValue={this.state.validation.invalidGender}
            gender={this.state.user.gender}
            updateGender={this.updateUserGender}
          />
          <Button main text="Próximo" onClick={this.valid} />
        </form>
      </div>
    );
  }
}

export default NewUser;

3.4 Finalizar a 1a tela de novo usuário

Sintetizando as 2 telas de novo usuário:

  1. Deve exibir para edição os campos de nome e gênero, e o botão próximo;
  2. Deve exibir o campo nome, permitir escolher um avatar, e os botões voltar e salvar.

Quais etapas para preparar a 1a tela:

  1. Refatorar NewUser, encapsulando as entradas (jsx dentro de render) com métodos:
    • renderName
    • renderGender
    • renderButtons

src/components/NewUser/NewUser.js

import React, { Component } from "react";

import Label from "../Label";
import Input from "../Input";
import GenderSelector from "../GenderSelector";
import Button from "../Button";
import User from "../../models/User";

class NewUser extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: new User(),
      validation: {
        invalidName: false,
        invalidGender: false
      },
      completedFirstView: false
    };
  }

  updateUserName = event => {
    let user = this.state.user;
    user.name = event.target.value;
    this.setState({
      user: user
    });
  };

  updateUserGender = (event, gender) => {
    event.preventDefault();
    let user = this.state.user;
    user.gender = gender;
    this.setState({
      user: user
    });
  };

  valid = e => {
    e.preventDefault();
    let user = this.state.user;
    let validation = this.state.validation;

    validation.invalidName = !user.validName();
    validation.invalidGender = !user.validGender();

    let message = "";
    let completedFirstView = false;

    if (validation.invalidName && validation.invalidGender) {
      message = "Por favor, informe seu nome e gênero!!!";
    } else if (validation.invalidName) {
      message = "Por favor, informe seu nome!!!";
    } else if (validation.invalidGender) {
      message = "Por favor, selecione seu gênero!!!";
    } else {
      completedFirstView = true;
    }

    if (!completedFirstView) {
      this.props.error(message);
    }

    this.setState({
      validation: validation,
      completedFirstView: completedFirstView
    });
  };

  renderName() {
    return (
      <section>
        <Label
          htmlFor="name"
          text="Quem é você?"
          invalidValue={this.state.validation.invalidName}
        />
        <Input
          id="name"
          placeholder="Digite seu nome"
          maxLength="40"
          readOnly={this.state.completedFirstView}
          invalidValue={this.state.validation.invalidName}
          defaultValue={this.state.user.name}
          onChange={this.updateUserName}
        />
      </section>
    );
  }

  renderGender() {
    if (this.state.completedFirstView) {
      return null;
    } else {
      return (
        <section>
          <Label
            text="Seu gênero:"
            invalidValue={this.state.validation.invalidGender}
          />
          <GenderSelector
            invalidValue={this.state.validation.invalidGender}
            gender={this.state.user.gender}
            updateGender={this.updateUserGender}
          />
        </section>
      );
    }
  }

  renderButtons() {
    if (this.state.completedFirstView) {
      return (
        <section>
          <Button
            text="Voltar"
            onClick={event => {
              event.preventDefault();
              this.setState({
                completedFirstView: false
              });
            }}
          />
          <Button text="Salvar" main />
        </section>
      );
    } else {
      return (
        <section>
          <Button main text="Próximo" onClick={this.valid} />
        </section>
      );
    }
  }

  render() {
    return (
      <div className="center">
        <form className="pure-form pure-form-stacked">
          {this.renderName()}
          {this.renderGender()}
          {this.renderButtons()}
        </form>
      </div>
    );
  }
}

export default NewUser;

4. Criar e testar o componente ImageScroller

Sub-tarefas:

  1. Criar o componente src/components/ButtonImage
  2. Entender o funcionamento do src/components/ImageScroller
  3. Criar o arquivo src/components/ImageScroller/ManipularEvento.js
  4. Criar a classe de domínio src/models/Avatar.js
  5. Modificar o componente src/components/ImageScroller
# Fechar o servidor web
## Pressione CTRL+C

# copiar imagem para o diretório public/img
curl https://raw.githubusercontent.com/tiipos/tiipos.github.io/master/react/img/behappy/botoes.png -o public/img/buttons.png

# Criar diretório do componetne ButtonImage e os arquivos
mkdir src/components/ButtonImage
touch src/components/ButtonImage/index.js
touch src/components/ButtonImage/index.css
touch src/components/ButtonImage/ButtonImage.js

# Criar diretório do componetne ImageScroller e os arquivos
mkdir src/components/ImageScroller
touch src/components/ImageScroller/index.js
touch src/components/ImageScroller/ManipularEvento.js
touch src/components/ImageScroller/ImageScroller.js

touch src/models/Avatar.js

4.1 Criar o componente ButtonImage

Sprites do componente ButtonImage

src/components/ButtonImage/index.js

import ButtonImage from "./ButtonImage";

export default ButtonImage;

src/components/ButtonImage/index.css

div.option-image-scroller {
  border-radius: 50%;
  padding: 4px;
  background-color: #dbdbdb;
  margin: 62px 2px 0 2px;
}

div.option-image-scroller:hover {
  background-color: #8d8d8d;
}

src/components/ButtonImage/ButtonImage.js

import React from "react";

import "./index.css";

import Image from "../Image";

export default function ButtonImage(props) {
  let style = {};
  let index = 0;
  const size = 48;

  if (props.position === "right") {
    style.float = "right";
    index = 1;
  } else {
    style.float = "left";
    index = 0;
  }

  let properties = Object.assign({}, props);
  delete properties.position;

  return (
    <div style={style} className="option-image-scroller" {...properties}>
      <Image
        y={0}
        x={index}
        width={size}
        height={size}
        backgroundHeight={size}
        file="img/buttons.png"
      />
    </div>
  );
}

4.2 Entender o funcionamento de ImageScroller

Estrutura do ImageScroller

Estrutura do elemento UL de ImageScroller

Comportamento do ImageScroller (```ManipularEvento.js```)

4.3 Criar o arquivo ManipularEvento.js

src/components/ImageScroller/ManipularEvento.js

class ManipularEvento {
  constructor(qtdeElementos, index) {
    this.maxIndex = qtdeElementos;
    this.index = index;

    this.comprimentoItem = 140;
    this.maxLeft = 86;
    this.left = this.maxLeft;

    this.minLeft =
      (qtdeElementos - 1) * this.comprimentoItem * -1 + this.maxLeft;

    this.minIndex = 0;
    this.direcao = 0;
    this.deslocamento = 0;
    this.toqueInicial = 0;
    this.toqueAnterior = 0;
    this.toqueEmExecucao = false;
  }

  definirIndex(index) {
    if (index >= this.minIndex && index < this.maxIndex) {
      this.index = index;
    }
  }

  iniciar(x) {
    this.deslocamento = this.left;
    this.toqueInicial = x;
    this.toqueEmExecucao = true;
  }

  mover(toqueX) {
    if (this.toqueEmExecucao) {
      this.swipe(toqueX);
      this.flinging(toqueX);
      this.calcularDirecao(toqueX);
      this.toqueAnterior = toqueX;
    }
  }

  atualizarClique() {
    this.atualizar(false);
  }
  atualizarToque() {
    this.atualizar(true);
  }

  atualizar(toque) {
    this.toqueInicial = 0;
    this.toqueEmExecucao = false;
    if (toque) this.corrigirIndex();
    this.left = this.index * this.comprimentoItem * -1 + this.maxLeft;
  }

  swipe(toqueX) {
    let deltaX = toqueX - this.toqueInicial + this.deslocamento;
    if (deltaX < this.minLeft) {
      deltaX = this.minLeft;
    } else if (deltaX > this.maxLeft) {
      deltaX = this.maxLeft;
    }

    this.left = deltaX;
  }

  flinging(toqueX) {
    let index = Math.round(
      Math.abs((this.left - this.maxLeft) / this.comprimentoItem)
    );

    let diferenca = toqueX - this.toqueAnterior;
    let extensaoToque = Math.abs(diferenca);

    let bonus = 0;
    if (extensaoToque < this.comprimentoItem) {
      if (diferenca < 0) {
        bonus = 1;
      } else {
        bonus = -1;
      }
    }
    this.definirIndex(index + bonus);
  }

  calcularDirecao(toqueX) {
    if (toqueX > this.toqueAnterior) {
      this.direcao = 1;
    } else if (toqueX < this.toqueAnterior) {
      this.direcao = -1;
    } else {
      this.direcao = 0;
    }
  }

  corrigirIndex() {
    if (this.index === 1 && this.direcao === 1 && this.left === this.maxLeft) {
      this.index = 0;
    }

    if (
      this.index === this.maxIndex - 2 &&
      this.direcao === -1 &&
      Math.sign(this.left) === -1
    ) {
      this.index = this.maxIndex - 1;
    }
  }
}

export default ManipularEvento;

4.4 Criar a classe de domínio Avatar.js

src/models/Avatar.js

class Avatar {
  constructor(index, description) {
    this.index = index;
    this.description = description;
  }

  toString() {
    return this.description;
  }

  static getAll() {
    return Array(23)
      .fill(0)
      .map((entry, index) => {
        return new Avatar(index, `Avatar ${index + 1}`);
      });
  }
}

export default Avatar;

src/models/User.js atualizando modelo User

import Avatar from "./Avatar";

class User {
  constructor() {
    this.name = "";
    this.gender = "";
    this.avatar = Avatar.getAll()[0];
  }

  validName = () => {
    return (
      typeof this.name === "string" &&
      this.name.length !== 0 &&
      this.name.length <= 40
    );
  };

  validGender = () => {
    return ["m", "f"].some(param => {
      return this.gender === param;
    });
  };
}

export default User;

4.5 ImageScroller

src/components/ImageScroller/index.js

import ImageScroller from "./ImageScroller";

export default ImageScroller;

Descrevendo os métodos renders do ImageScroller

src/components/ImageScroller/ImageScroller.js : props[file, y, images, selectedImage, onClick]

import React from "react";
import Image from "../Image";
import ButtonImage from "../ButtonImage";
import ManipularEvento from "./ManipularEvento";

class ImageScroller extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      eventHandler: new ManipularEvento(
        this.props.images.length,
        this.props.selectedImage.index
      )
    };
  }

  getAll() {
    return this.props.images[this.state.eventHandler.index];
  }

  renderButtonImage(position) {
    return (
      <ButtonImage
        position={position}
        onTouchStart={e => e.stopPropagation()}
        onTouchMove={e => e.stopPropagation()}
        onTouchEnd={e => e.stopPropagation()}
        onClick={e => {
          e.preventDefault();
          let eventHandler = this.state.eventHandler;
          let index = eventHandler.index;
          if (position === "left") {
            index += -1;
          } else {
            index += 1;
          }
          eventHandler.definirIndex(index);
          eventHandler.atualizarClique();

          this.setState({ eventHandler: eventHandler }, () => {
            this.props.onChange(this.getAll());
          });
        }}
      />
    );
  }

  renderSelected() {
    let style = {
      float: "left",
      width: "140px",
      height: "160px",
      marginLeft: "42px",
      backgroundColor: "#00C853",
      position: "relative",
      zIndex: -2
    };

    return <span style={style} />;
  }

  renderImage(entry, index) {
    let y = this.props.y ? this.props.y : 0;
    let style = {
      paddingTop: "8px",
      position: "absolute",
      zIndex: "-1",
      marginLeft: `${index * 140}px`
    };

    return (
      <li style={style} key={index + entry.toString()}>
        <Image
          x={entry.index}
          y={y}
          width={170}
          height={170}
          backgroundHeight={280}
          file={this.props.file}
        />
      </li>
    );
  }

  renderImages() {
    const ms = this.state.eventHandler.toqueEmExecucao ? "100ms" : "800ms";

    const style = {
      WebkitTransitionDuration: ms /* Safari e Chrome */,
      MsTransitionDuration: ms /* IE */,
      MozTransitionDuration: ms /* Firefox */,
      OTransitionDuration: ms /* Opera */,
      transitionDuration: ms /* Nativa do W3C */,

      listStyleType: "none",
      margin: "0",
      padding: "0",
      position: "relative",
      width: `${this.props.images.length * 170}px`,
      left: `${this.state.eventHandler.left}px`
    };

    const lista = this.props.images.map((entry, index) =>
      this.renderImage(entry, index)
    );

    return <ul style={style}>{lista}</ul>;
  }

  renderImageScroller() {
    const style = {
      boxSizing: "border-box",
      borderWidth: "1px",
      borderBottomWidth: "0",
      borderStyle: "solid",
      borderColor: "#cccccc",
      borderRadius: "5px",
      borderBottomLeftRadius: "0",
      borderBottomRightRadius: "0",
      width: "310px",
      height: "160px",
      overflow: "hidden"
    };

    return (
      <div
        style={style}
        onTouchStart={this.onTouchStart.bind(this)}
        onTouchMove={this.onTouchMove.bind(this)}
        onTouchEnd={this.onTouchEnd.bind(this)}
      >
        {this.renderButtonImage("left")}
        {this.renderSelected()}
        {this.renderImages()}
        {this.renderButtonImage("right")}
      </div>
    );
  }

  renderLabel() {
    const style = {
      boxSizing: "border-box",
      borderWidth: "1px",
      borderStyle: "solid",
      borderTopWidth: "0",
      borderColor: "#cccccc",
      borderRadius: "5px",
      borderTopLeftRadius: "0",
      borderTopRightRadius: "0",
      backgroundColor: "#cccccc",
      color: "#444444",
      fontSize: "20px",
      textAlign: "center",
      padding: "5px",
      width: "310px"
    };

    return <div style={style}>{this.getAll().toString()}</div>;
  }

  onTouchStart(e) {
    let clientX = e.targetTouches[0].clientX;
    let eventHandler = this.state.eventHandler;
    eventHandler.iniciar(clientX);
    this.setState({ eventHandler: eventHandler });
  }
  onTouchMove(e) {
    let clientX = e.targetTouches[0].clientX;
    let eventHandler = this.state.eventHandler;
    eventHandler.mover(clientX);
    this.setState({ eventHandler: eventHandler });
  }
  onTouchEnd(e) {
    let eventHandler = this.state.eventHandler;
    eventHandler.atualizarToque();
    this.setState({ eventHandler: eventHandler }, () => {
      this.props.onChange(this.getAll());
    });
  }

  render() {
    return (
      <div>
        {this.renderImageScroller()}
        {this.renderLabel()}
      </div>
    );
  }
}

export default ImageScroller;

src/components/NewUser/NewUser.js

import React, { Component } from "react";

import Label from "../Label";
import Input from "../Input";
import GenderSelector from "../GenderSelector";
import Button from "../Button";
import Toast from "../Toast";
import ImageScroller from "../ImageScroller";

import User from "../../models/User";
import Avatar from "../../models/Avatar";

class NewUser extends Component {
  constructor(props) {
    super(props);
    this.state = {
      user: new User(),
      validation: {
        invalidName: false,
        invalidGender: false
      },
      completedFirstView: false
    };
  }

  updateUserName = event => {
    let user = this.state.user;
    user.name = event.target.value;
    this.setState({
      user: user
    });
  };

  updateUserGender = (event, gender) => {
    event.preventDefault();
    let user = this.state.user;
    user.gender = gender;
    user.avatar = Avatar.getAll()[0];
    this.setState({
      user: user
    });
  };

  valid = e => {
    e.preventDefault();
    let user = this.state.user;
    let validation = this.state.validation;

    validation.invalidName = !user.validName();
    validation.invalidGender = !user.validGender();

    let message = "";
    let completedFirstView = false;

    if (validation.invalidName && validation.invalidGender) {
      message = "Por favor, informe seu nome e gênero!!!";
    } else if (validation.invalidName) {
      message = "Por favor, informe seu nome!!!";
    } else if (validation.invalidGender) {
      message = "Por favor, selecione seu gênero!!!";
    } else {
      completedFirstView = true;
    }

    if (!completedFirstView) {
      this.props.error(message);
    }

    this.setState({
      validation: validation,
      completedFirstView: completedFirstView
    });
  };

  renderName() {
    return (
      <section>
        <Label
          htmlFor="name"
          text="Quem é você?"
          invalidValue={this.state.validation.invalidName}
        />
        <Input
          id="name"
          placeholder="Digite seu nome"
          maxLength="40"
          readOnly={this.state.completedFirstView}
          invalidValue={this.state.validation.invalidName}
          defaultValue={this.state.user.name}
          onChange={this.updateUserName}
        />
      </section>
    );
  }

  renderGender() {
    if (this.state.completedFirstView) {
      return null;
    } else {
      return (
        <section>
          <Label
            text="Seu gênero:"
            invalidValue={this.state.validation.invalidGender}
          />
          <GenderSelector
            invalidValue={this.state.validation.invalidGender}
            gender={this.state.user.gender}
            updateGender={this.updateUserGender}
          />
        </section>
      );
    }
  }

  renderButtons() {
    if (this.state.completedFirstView) {
      return (
        <section>
          <Button
            text="Voltar"
            onClick={event => {
              event.preventDefault();
              let user = this.state.user;
              user.avatar = Avatar.getAll()[0];
              this.setState({
                user: user,
                completedFirstView: false
              });
            }}
          />
          <Button
            main
            text="Salvar"
            onClick={event => {
              event.preventDefault();
              this.props.onSubmit(this.state.user);
            }}
          />
        </section>
      );
    } else {
      return (
        <section>
          <Button main text="Próximo" onClick={this.valid} />
        </section>
      );
    }
  }

  renderAvatar() {
    if (this.state.completedFirstView) {
      return (
        <section>
          <Label text="Escolha seu avatar:" />
          <ImageScroller
            file="img/avatars.png"
            y={this.state.user.gender === "m" ? 0 : 1}
            images={Avatar.getAll()}
            selectedImage={this.state.user.avatar}
            onChange={avatar => {
              let user = this.state.user;
              user.avatar = avatar;
              this.setState({ user: user });
            }}
          />
        </section>
      );
    } else {
      return null;
    }
  }

  render() {
    return (
      <div className="center">
        <form className="pure-form pure-form-stacked">
          {this.renderName()}
          {this.renderGender()}
          {this.renderAvatar()}
          {this.renderButtons()}
        </form>
      </div>
    );
  }
}

export default NewUser;

src/App.js

import React, { Component } from "react";
// import logo from './logo.svg';

import "./App.css";
import Header from "./components/Header";
import NewUser from "./components/NewUser";
import Toast from "./components/Toast";

class App extends Component {
  render() {
    return (
      <div>
        <Header />
        <NewUser
          onSubmit={user => {
            let gender = user.gender === "m" ? "o" : "a";
            this.refs.toast.success(`Seja bem vind${gender} ${user.name}!`);
          }}
          error={msg => this.refs.toast.error(msg)}
        />
        <Toast ref="toast" />
      </div>
    );
  }
}

export default App;

5. Registrando as mudanças

  1. Fechar servidor web CTRL + C
  2. Adicioanar novos e modificados arquivos (git add) no repositório local
  3. Registrar mudanças (git commit) no repositório local
  4. Publicar mudanças no repositório remoto (git pull e git push)
# Fechar o servidor web
### 1. Pressionar CTRL+C

### 2. Adicionar arquivos novos e modificados no repositório local
git add src public/img package.json

### 3. Registrar mudanças no repositório local
git commit -m "$MSG"

### Publicar as mudanças no repositório remoto
git pull
git push

6. Publicando no Heroku

  1. Acessar o Heroku e se autenticar
  2. Acessar o link do seu App no Heroku
  3. No dashboard do seu App, clicar na aba Deploy
  4. Em Deployment method, selecionar GiHub
  5. Definir qual o repositório esta o App
  6. Em Manual deploy, clicar em Deploy Branch
  7. Aguardar um tempo, olhando o log
  8. No topo da página, clicar em Open App

7. Homework

Just do it!!!