Skip to content

# Step 07 — Users component

Gerald Goh edited this page Aug 11, 2020 · 1 revision

Gravatar Setup

Gravatar stands for Globally Recognized Avatar. It is globally recognized because millions of people and websites use them. Most popular applications like WordPress have built-in support for Gravatar. When a user leaves a comment (with email) on a site that supports Gravatar, it pulls their Globally Recognized Avatar from Gravatar servers. Then that picture is shown next to the comment. This allows each commenter to have their identity through out the world wide web.

source

For the purpose of this project, Gravatar will be used profile pictures.

# app/helpers/users_helper.rb

module UsersHelper

  # Returns the Gravatar for the given user.
  def gravatar_for(user, size: 80)
    gravatar_id = Digest::MD5::hexdigest(user.email.downcase)
    gravatar_url = "https://secure.gravatar.com/avatar/#{gravatar_id}?s=#{size}"
    image_tag(gravatar_url, alt: user.name, class: "gravatar")
  end
end

Change Signup function to a class

Build Class and add constructor;
./app/javascript/components/home/Signup.jsx

class Signup extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '',
      email: '',
      password: '',
      password_confirmation: '',
      units: '',
      target: '',
      errors: {},
    }
  }
.
.
Add eventhandlers and functions
  1. handleChange to pass values to set state
  2. onSubmit send user inputs to users controlller (see config/routes.rb)
./app/javascript/components/Signup.jsx

.
.
  handleChange(e) {
    this.setState(
      {
        [e.target.id]: e.target.value
      }
    )
  }
  onSubmit(e) {
    e.preventDefault();
    let { name, email, password, password_confirmation, units, target } = this.state;
    
    axios.post("/api/users", {      
        name, email, password, password_confirmation, units, target,      
    })
      .then(response => response.data)
      .then(response => {
        if (response.code == 400) {
          console.log(response);
          this.setState({
            errors: response.errors,
          })
        } else if (response.code == 200) {
          this.setState({
            name: "",
            email: "",
            password: "",
            password_confirmation: '',
            units: "",
            target: "",
            errors: {},
          })
        }
      }
    )
  }
.
.
Build render and event listeners for signup
  1. Render DOM
  2. Listens for input and click events to pass functions on with argument.
./app/javascript/components/Signup.jsx
.
.
  render() {
    let { name, email, password, password_confirmation, units, target } = this.state;
    return (
      <div className="container text-content sign-in-up">
        <div className="row justify-content-center">
          <div className="col-md-4 col-md-offset-4">
            <br />
            {/* Nav tabs */}
            <div className="text-center">
              <div className="btn-group">
                <a href="#new" role="tab" data-toggle="tab" className="big btn btn-primary">
                  <i className="fa fa-plus" />
                  {' '}
                  Create Account
                </a>
                <a href="#user" role="tab" data-toggle="tab" className="big btn btn-success">
                  <i className="fa fa-user" />
                  {' '}
                  Login
                </a>
              </div>
            </div>
            <p className="click2select">Tap to select</p>
            <div className="tab-content">
              <div className="tab-pane fade in active" id="new">
                <br />
                <fieldset>
                  <div className="form-group">
                    <div className="right-inner-addon">
                      <i className="fa fa-user" />
                      <input
                        className="form-control input-lg" 
                        placeholder="Name" 
                        type="text" 
                        id="name"
                        value={name}
                        onChange={e => this.handleChange(e)}
                      />
                    </div>
                  </div>
                  <div className="form-group">
                    <div className="right-inner-addon">
                      <i className="fa fa-envelope" />
                      <input 
                        className="form-control input-lg" 
                        placeholder="Email" 
                        type="text"
                        id="email" 
                        value={email}
                        onChange={e => this.handleChange(e)}
                      />
                    </div>
                  </div>
                  <div className="form-group">
                    <div className="right-inner-addon">
                      <i className="fa fa-key" />
                      <input 
                        className="form-control input-lg" 
                        placeholder="Password" 
                        type="password"
                        id="password"
                        value={password}
                        onChange={e => this.handleChange(e)}
                      />
                    </div>
                  </div>
                  <div className="form-group">
                    <div className="right-inner-addon">
                      <i className="fa fa-key" />
                      <input 
                        className="form-control input-lg" 
                        placeholder="Password Confirmation" 
                        type="password"
                        id="password_confirmation"
                        value={password_confirmation}
                        onChange={e => this.handleChange(e)}
                      />
                    </div>
                  </div>
                  <div className="form-group">
                    <select 
                      className="custom-select custom-select-md"
                      id="units"
                      value={units}
                      onChange={e => this.handleChange(e)}
                    >
                      <option>Choose monthly units</option>
                      <option value="1800">1800</option>
                      <option value="2100">2100</option>
                      <option value="2400">2400</option>
                      <option value="2700">2700</option>
                      <option value="3000">3000</option>
                    </select>
                  </div>
                  <div className="form-group">
                    <select 
                      className="custom-select custom-select-md"
                      id="target"
                      value={target}
                      onChange={e => this.handleChange(e)}
                    >
                      <option>Choose month target savings</option>
                      <option value="5">5%</option>
                      <option value="10">10%</option>
                      <option value="15">15%</option>
                      <option value="20">20%</option>
                      <option value="25">25%</option>
                    </select>
                  </div>
                </fieldset>
                <hr />
                <div className="tab-content">
                  <div className="tab-pane active text-center" id="pp">
                    <button 
                      className="btn btn-primary btn-lg btn-block" 
                      type="submit"
                      onClick={(e) => this.onSubmit(e)}
                    >
                      <i className="fa fa-plus" />
                      {' '}
                      Create Account
                    </button>
                  </div>
                </div>
              </div>
              <Signin />
            </div>
          </div>
        </div>
      </div>
    )
  }
.
.

Split Signin code block from Signup component

  1. Added Axios
  2. Added Redux connect
  3. Added dispatch SIGNEDIN action that stores state function
  4. Added eventlisteners for click events and input
  5. Added constructors
  6. Changed function to class
./app/javascript/components/home/Signin.jsx

/* eslint-disable react/destructuring-assignment */
/* eslint-disable react/no-unused-state */
import React from 'react';
import axios from 'axios';
import PropTypes from 'prop-types';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { SIGNEDIN } from '../../actions';


class Signin extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      email: '',
      password: '',
      errors: {},
      success: true,
    };
  }

  onSubmit(e) {
    e.preventDefault();
    const { email, password } = this.state;
    axios.post('/api/users/sessions', {
      user: {
        email,
        password,
      },
    })
      .then((response) => response.data)
      .then((response) => {
        if (response.code === 200) {
          this.props.login(response.user.name);
          this.props.history.push('/addreading');
        } else if (response.code === 400) {
          this.setState({
            errors: response.errors,
          });
        }
      });
  }

  handleChange(e) {
    this.setState(
      {
        [e.target.id]: e.target.value,
      },
    );
  }

  render() {
    const { email, password } = this.state;

    return (
      <div className="tab-pane" id="user">
        <br />
        <fieldset>
          <div className="form-group">
            <div className="right-inner-addon">
              <i className="fa fa-envelope" />
              <input
                className="form-control input-lg"
                placeholder="Email"
                type="text"
                id="email"
                value={email}
                onChange={(e) => this.handleChange(e)}
              />
            </div>
          </div>
          <div className="form-group">
            <div className="right-inner-addon">
              <i className="fa fa-key" />
              <input
                className="form-control input-lg"
                placeholder="Password"
                type="password"
                id="password"
                value={password}
                onChange={(e) => this.handleChange(e)}
              />
            </div>
          </div>
        </fieldset>
        <br />
        <div className=" text-center">
          <button
            className="btn btn-primary btn-success"
            type="button"
            onClick={(e) => this.onSubmit(e)}
          >
            <i className="fa fa-user" />
            {' '}
            LOGIN
          </button>
        </div>
      </div>
    );
  }
}

const mapDispatchToProps = (dispatch) => ({
  login: ((name) => {
    dispatch(SIGNEDIN(name));
  }),
});

Signin.propTypes = {
  login: PropTypes.func.isRequired,
  // eslint-disable-next-line react/forbid-prop-types
  history: PropTypes.object.isRequired,
};

export default withRouter(connect(null, mapDispatchToProps)(Signin));

Signup component

./app/javascript/components/home/Signup.jsx

/* eslint-disable react/no-unused-state */
/* eslint-disable camelcase */
import React from 'react';
import axios from 'axios';

class Signup extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      name: '',
      email: '',
      password: '',
      password_confirmation: '',
      units: '',
      target: '',
      errors: {},
    };
  }

  onSubmit(e) {
    e.preventDefault();
    const {
      name, email, password, password_confirmation, units, target,
    } = this.state;
    axios.post('/api/users', {
      name, email, password, password_confirmation, units, target,
    })
      .then((response) => response.data)
      .then((response) => {
        if (response.code === 400) {
          this.setState({
            errors: response.errors,
          });
        } else if (response.code === 200) {
          this.setState({
            name: '',
            email: '',
            password: '',
            password_confirmation: '',
            units: '',
            target: '',
            errors: {},
          });
        }
      });
  }

  handleChange(e) {
    this.setState(
      {
        [e.target.id]: e.target.value,
      },
    );
  }

  render() {
    const {
      name, email, password, password_confirmation, units, target,
    } = this.state;
    return (
      <div className="tab-content">
        <div id="new">
          <br />
          <fieldset>
            <div className="form-group">
              <div className="right-inner-addon">
                <i className="fa fa-user" />
                <input
                  className="form-control input-lg"
                  placeholder="Name"
                  type="text"
                  id="name"
                  value={name}
                  onChange={(e) => this.handleChange(e)}
                />
              </div>
            </div>
            <div className="form-group">
              <div className="right-inner-addon">
                <i className="fa fa-envelope" />
                <input
                  className="form-control input-lg"
                  placeholder="Email"
                  type="text"
                  id="email"
                  value={email}
                  onChange={(e) => this.handleChange(e)}
                />
              </div>
            </div>
            <div className="form-group">
              <div className="right-inner-addon">
                <i className="fa fa-key" />
                <input
                  className="form-control input-lg"
                  placeholder="Password"
                  type="password"
                  id="password"
                  value={password}
                  onChange={(e) => this.handleChange(e)}
                />
              </div>
            </div>
            <div className="form-group">
              <div className="right-inner-addon">
                <i className="fa fa-key" />
                <input
                  className="form-control input-lg"
                  placeholder="Password Confirmation"
                  type="password"
                  id="password_confirmation"
                  value={password_confirmation}
                  onChange={(e) => this.handleChange(e)}
                />
              </div>
            </div>
            <div className="form-group">
              <select
                className="custom-select custom-select-md"
                id="units"
                value={units}
                onChange={(e) => this.handleChange(e)}
              >
                <option>Choose monthly units</option>
                <option value="1800">1800</option>
                <option value="2100">2100</option>
                <option value="2400">2400</option>
                <option value="2700">2700</option>
                <option value="3000">3000</option>
              </select>
            </div>
            <div className="form-group">
              <select
                className="custom-select custom-select-md"
                id="target"
                value={target}
                onChange={(e) => this.handleChange(e)}
              >
                <option>Choose month target savings</option>
                <option value="5">5%</option>
                <option value="10">10%</option>
                <option value="15">15%</option>
                <option value="20">20%</option>
                <option value="25">25%</option>
              </select>
            </div>
          </fieldset>
          <hr />
          <div className="tab-content">
            <div className="tab-pane active text-center" id="pp">
              <button
                className="btn btn-primary btn-lg btn-block"
                type="submit"
                onClick={(e) => this.onSubmit(e)}
              >
                <i className="fa fa-plus" />
                {' '}
                Create Account
              </button>
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default Signup;

Refactor Home container

.app/javascript/containers/Home.jsx

/* eslint-disable react/destructuring-assignment */
import React from 'react';
import Head from '../components/home/Head';
import Signin from '../components/home/Signin';
import Signup from '../components/home/Signup';

class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      section: <Signin />,
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(e) {
    const sector = e.target.value;
    if (sector === 'Signup') {
      this.setState(() => ({
        section: <Signup />,
      }));
    } else {
      this.setState(() => ({
        section: <Signin />,
      }));
    }
  }

  render() {
    const sector = this.state.section;
    return (
      <div>
        <Head />
        <div className="container text-content sign-in-up">
          <div className="row justify-content-center">
            <div className="col-md-4 col-md-offset-4">
              <br />
              {/* Nav tabs */}
              <div className="text-center">
                <div className="btn-group">
                  <button
                    type="button"
                    value="Signup"
                    className="big btn btn-primary"
                    onClick={(e) => this.handleClick(e, 'value')}
                  >
                    <i className="fa fa-plus" />
                    {' '}
                    Create Account
                  </button>
                  <button
                    type="button"
                    value="Signin"
                    className="big btn btn-success"
                    onClick={(e) => this.handleClick(e, 'value')}
                  >
                    <i className="fa fa-user" />
                    {' '}
                    Login
                  </button>
                </div>
              </div>
              <p className="click2select">Tap to select</p>
              {sector}
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default Home;

Setup User controller

$ rails generate controller api/v1/users index create show -j=false -y=false --skip-template-engine --no-helper
      create  app/controllers/users_controller.rb
      invoke  erb
      create    app/views/users
      invoke  test_unit
      create    test/controllers/users_controller_test.rb
      invoke  helper
      create    app/helpers/users_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/users.scss

Edit User controller

  1. To accept params from react
app/controllers/users_controller.rb

class Api::V1::UsersController < ApplicationController
  skip_before_action :verify_authenticity_token
  
  def create
    @user = User.new(
      name: params[:name],
      email: params[:email],
      password: params[:password],
      password_confirmation: params[:password_confirmation],
      units: params[:units],
      target: params[:target],
    )
    if @user.save
      render json: {
        code: 200
      }
    else
      render json: {
        code: 400,
        errors: @user.errors.messages
      }
    end
  end

  def index
    @users = User.all
    if @users
      render json: {
        code: 200,
        data: User.all.as_json
      }
    else
      render json: @users.errors
    end
  end

  def find_user
    @users = User.all
    @users = User.find(params[:id])
    if @users
      render json: {
        code: 200,
        data: @users.as_json
      }
    else
      render json: @users.errors
    end
  end

  def destroy; end
end

Build Sessions Controller

$ rails generate controller api/v1/users/sessions -j=false -y=false --skip-template-engine --no-helper
      create  app/controllers/users/sessions_controller.rb
      invoke  erb
      create    app/views/users/sessions
      invoke  test_unit
      create    test/controllers/users/sessions_controller_test.rb
      invoke  helper
      create    app/helpers/users/sessions_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/users/sessions.scss

Edit Session controller

  1. To accept params from react
app/controllers/api/v1/users/sessions_controller.rb

class Api::V1::Users::SessionsController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    @user = User.find_by(email: params[:user][:email])
    if @user&.authenticate(params[:user][:password])
      login @user
      render json: {
        code: 200,
        user: {
          name: @user.name
        }
      }
    else
      render json: {
        code: 400,
        errors: { Error: ['Email and/or Password does not match !'] }
      }
    end
  end

  def destroy
    @current_user = nil
    session.delete(:id)
  end

  private

  def user_params
    params[:user].permit(:email, :password)
  end
end

Build action

Added new file and content

/app/javascript/actions/index.jsx

const SIGNEDIN = (name) => ({
  type: 'SIGNEDIN',
  name,
});
const SIGNEDOUT = () => ({ type: 'SIGNEDOUT' });

export { SIGNEDIN, SIGNEDOUT };

Build reducer

/app/javascript/reducer/sessions.jsx\

const sessionReducer = (state = { siginin: false, name: '' }, action) => {
  switch (action.type) {
    case 'SIGNEDIN':
      return { login: true, name: action.name };
    case 'SIGNEDOUT':
      return { login: false, name: '' };
    default:
      return state;
  }
};

export default sessionReducer;

Add routes

  1. Add redux store and provider
app/javscript/routes/index.jsx

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Provider } from 'react-redux';
import { compose, createStore } from 'redux';
import persistState from 'redux-localstorage';
import Home from '../containers/Home';
import Readings from '../containers/Readings';
import AddReading from '../containers/AddReading';
import Reading from '../containers/Reading';
import reducer from '../reducer/sessions';


const store = createStore(
  reducer, compose(persistState()),
);

export default (
  <Provider store={store}>
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/addreading" component={AddReading} />
        <Route path="/readings" component={Readings} />
        <Route path="/reading" component={Reading} />
      </Switch>
    </Router>
  </Provider>
);

export { store };

Edit application controller

app/controller/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  def login(user)
    session[:id] = user.id
  end

  def current_user
    @current_user ||= session[:id] && User.find_by_id(session[:id])
  end

  def logged_in?
    render json: { code: 401 } unless current_user
  end
end