Scout - React Redux Project

Posted by Alisa Cookie McCormick on July 20, 2020

Finally, the final project!!!

I took some time off after the Rails Javascript Project. I passed the assessment and then had a baby a few hours later. Life has been pretty busy since and it’s been hard finding the time to code. So, I’m really glad to be able to finish all assignments and projects.

For my React Redux final project, I created a CRUD app called Scout, which lists models and their bookings. This app is the beginning of what I would have liked in my previous position as Talent Scheduling Specialist at HSN. I was responsible for finding models to demo products on TV. I didn’t have any sort of special app to help with my daily duties. Model pictures were in folders on my desktop. Bookings were done on spreadsheets and emailed. I had comp cards in a folder. It would’ve been nice the information in one place.

Scout

Scout is a database for models, their stats and bookings.

Backend

Scout uses an Rails API backend to handle the data persistence with the database. Two models were created, a model model (sounds confusing) and a booking model.

class Model < ApplicationRecord
  has_many :bookings
  has_one_attached :picture

  validates :name, presence: true
  validates :height, presence: true
  validates :bust, presence: true, inclusion: { in: 20..50, message: 'must be 20-50' }
  validates :waist, presence: true, inclusion: { in: 20..50, message: 'must be 20-50' }
  validates :hip, presence: true,  inclusion: { in: 20..50, message: 'must be 20-50' }
  validates :shoe, presence: true, inclusion: { in: 5..12, message: 'must be 5-12' }
  validates :eyes, presence: true
  validates :hair, presence: true

  scope :alphabetical_name, -> { order("name asc") }
end
class Booking < ApplicationRecord
  belongs_to :model

  validates :model_id, presence: true
  validates :job, presence: true
  validates :amount, presence: true
  validates :start_time, presence: true
  validates :end_time, presence: true
  validates :description, presence: true

  scope :most_recent, -> { order("start_time desc") }
end

Since bookings are all associated with a model, routes and serializers are nested:

Rails.application.routes.draw do
  default_url_options host: "localhost", port: 3000

  namespace :api do
    namespace :v1 do
      resources :models do
        resources :bookings
      end
    end
  end
end
class ModelSerializer < ActiveModel::Serializer
  include Rails.application.routes.url_helpers

  attributes :id, :picture, :name, :height, :bust, :waist, :hip, :shoe, :eyes, :hair, :bookings

  has_many :bookings

  def picture
    return unless object.picture.attached?
    url_for(object.picture)
  end

  def bookings
    object.bookings.most_recent
  end
end
class BookingSerializer < ActiveModel::Serializer
  attributes :id, :job, :amount, :start_time, :end_time, :description, :model_id

  belongs_to :model

  def start_time
    object.start_time.strftime('%FT%R')
  end

  def end_time
    object.end_time.strftime('%FT%R')
  end
end

Frontend

I used Create React App to setup the frontend project. For styling, I used React Bootstrap.

The model form is used for both created a new model and updating a model. Below is the example of how a model is created:

import React from 'react';
import Form from 'react-bootstrap/Form';
import Button from 'react-bootstrap/Button';

const HEIGHTS = [
  '5\'7"',
  '5\'8"',
  '5\'9"',
  '5\'10"',
  '5\'11"',
  '6\'',
  '6\'1"',
  '6\'2"',
  '6\'3"',
  '6\'4"',
  '6\'5"'
];

class ModelForm extends React.Component {
  state = {
    name: this.props.model.name || '',
    picture: '',
    height: this.props.model.height || '',
    bust: this.props.model.bust || '',
    waist: this.props.model.waist || '',
    hip: this.props.model.hip || '',
    shoe: this.props.model.shoe || '',
    eyes: this.props.model.eyes || '',
    hair: this.props.model.hair || ''
  };

  handleChange = (event) => {
    this.setState({
      [event.target.name]: event.target.value
    });
  }

  handleOnSubmit = (event) => {
    event.preventDefault();

    const model = {...this.state, id: this.props.model.id};
    const formData = new FormData(event.target);

    this.props.onSubmit(formData, model);
  }

  render() {
    return (
      <div>
        <Form onSubmit={this.handleOnSubmit} encType="multipart/form-data">
          <Form.Group controlId="formName">
            <Form.Label>Model Name:</Form.Label>
            <Form.Control type="text" placeholder="Enter name" name="name" onChange={this.handleChange} value={this.state.name} required/>
          </Form.Group>
          <Form.Group controlId="formPicture">
            <Form.Label>Picture:</Form.Label>
            <Form.Control type="file" name="picture" onChange={this.handleChange}/>
          </Form.Group>
          <Form.Group controlId="formHeight">
            <Form.Label>Height:</Form.Label>
            <Form.Control as="select" value={this.state.height} onChange={this.handleChange} name='height'>
              {HEIGHTS.map(value => {
                return (
                  <option key={value}>{value}</option>
                );
              })}
            </Form.Control>
          </Form.Group>
          <Form.Group controlId="formBust">
            <Form.Label>Bust:</Form.Label>
            <Form.Control type="number" placeholder="Enter Bust" name="bust" onChange={this.handleChange} value={this.state.bust} required min="20" max="50"/>
          </Form.Group>
          <Form.Group controlId="formWaist">
            <Form.Label>Waist:</Form.Label>
            <Form.Control type="number" placeholder="Enter Waist" name="waist" onChange={this.handleChange} value={this.state.waist} required min="20" max="50"/>
          </Form.Group>
          <Form.Group controlId="formHip">
            <Form.Label>Hip:</Form.Label>
            <Form.Control type="number" placeholder="Enter Hip" name="hip" onChange={this.handleChange} value={this.state.hip} required min="20" max="50"/>
          </Form.Group>
          <Form.Group controlId="formHip">
            <Form.Label>Shoe:</Form.Label>
            <Form.Control type="number" placeholder="Enter Shoe" name="shoe" onChange={this.handleChange} value={this.state.shoe} required min="5" max="12"/>
          </Form.Group>
          <Form.Group controlId="formEyes">
            <Form.Label>Eyes:</Form.Label>
            <Form.Control type="text" placeholder="Enter Eyes" name="eyes" onChange={this.handleChange} value={this.state.eyes} required/>
          </Form.Group>
          <Form.Group controlId="formHair">
            <Form.Label>Hair:</Form.Label>
            <Form.Control type="text" placeholder="Enter Hair" name="hair" onChange={this.handleChange} value={this.state.hair}/>
          </Form.Group>
          <Button variant="primary" type="submit">
            Submit
          </Button>
        </Form><br/>
      </div>
    );
  }
}

export default ModelForm;

mapStateToProps is used to connect the application’s state to the model prop.

import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';

import { addModel } from '../actions/addModel';
import ModelForm from './ModelForm';

class ModelInput extends React.Component {
  handleOnSubmit = (formData, model) => {
    this.props.addModel(formData, model);
  }

  render() {
    //after model is created, redirect to model show page
    if (this.props.model) {
      return <Redirect to={`/models/${this.props.model.id}`}/>;
    }

    return (
      <div>
        <h4>Create Model:</h4>
        <ModelForm onSubmit={this.handleOnSubmit} model= />
      </div>
    );
  }
}

const mapStateToProps = state => {
  return {
    model: state.model
  };
}

export default connect(mapStateToProps, { addModel })(ModelInput);

The action makes a fetch() POST request to the RAILS API and converts to JSON. The action creator is dispatched.

export const addModel = (formData, data) => {
  return (dispatch) => {
    fetch('http://localhost:3000/api/v1/models', {
      method: 'POST',
      headers: {
        'Accept': 'application/json'
      },
      body: formData
    })
    .then(response => response.json())
    .then(model => dispatch({
      type: 'ADD_MODEL',
      payload: model
    }))
  };
}

The reducer returns the updated state.

function manageModels(state = {models: []}, action) {
  switch (action.type) {
    case 'ADD_MODEL':
      return {model: action.payload};
    default:
      return state;
  }
}

export default manageModels;

Feel free to check out the application.

React Redux Application Link:

https://github.com/cookiemccormick/scout