sombreroEnPuntas/car-gallery

View on GitHub
pages/index.js

Summary

Maintainability
A
1 hr
Test Coverage
// @flow

import React, { Component, Fragment } from 'react'

// Data
import { getMakes, getModels, getVehicles } from '../data/car-service'
import {
  getCarOptionsByKey,
  getCarsFiltered,
  getScore,
  isVehicleKey,
  makeFieldHelper,
} from '../data/form-data'
import type { VehicleT } from '../data/car-service'

// Components
import DropdownField from '../components/dropdown-field'
import Form from '../components/form'
import FormButtons from '../components/wrappers/FormButtons'
import FormSection from '../components/wrappers/FormSection'
import GitHubLink from '../components/wrappers/GitHubLink'
import Score from '../components/wrappers/Score'
import SelectField from '../components/select-field'

// Utils
import { assocPath, compose, dissocPath, path, pathSatisfies } from 'ramda'

const threeSecondsInMiliSeconds = 1000 * 3

type FieldDataT = {
  valid: boolean,
  value: string,
  values: [string] | [VehicleT],
}

type PropsT = {}

type StateT = {
  error: ?string,
  formData: { make: FieldDataT, model: FieldDataT, vehicle: FieldDataT },
  isLoading: boolean,
  retries: number,
  step: number,
}

export type HandleUpdateT = (string, string) => void
export type FetchAsyncDataT = () => *

class Index extends Component<PropsT, StateT> {
  state = {
    error: null,
    formData: {},
    isLoading: true,
    retries: 0,
    step: 1,
  }

  async componentDidMount() {
    try {
      await this.fetchAsyncData()
    } catch (error) {
      // unmount!
    }
  }

  async componentDidUpdate() {
    try {
      await this.fetchAsyncData()
    } catch (error) {
      // unmount!
    }
  }

  fetchAsyncData: FetchAsyncDataT = async () => {
    const {
      error,
      formData: { make, model, vehicle },
      retries,
    } = this.state

    // Did it fail already?
    if (error) {
      retries < 3 &&
        setTimeout(
          () =>
            this.setState(prevState => ({
              error: null,
              retries: prevState.retries + 1,
            })),
          threeSecondsInMiliSeconds
        )

      return
    }

    let key = ''
    let result

    try {
      // should fetch makes?
      if (!make || !make.values) {
        key = 'make'
        result = await getMakes()
      }
      // should fetch models?
      else if (!!make.value && make.valid && (!model || !model.values)) {
        key = 'model'
        result = await getModels(make.value)
      }
      // should fetch vehicles?
      else if (
        !!make.value &&
        make.valid &&
        !!model.value &&
        model.valid &&
        (!vehicle || !vehicle.values)
      ) {
        key = 'vehicle'
        result = await getVehicles(make.value, model.value)
      }
      // nothing to do here!
      else {
        return
      }
    } catch (error) {
      result = error
    }

    this.setState(
      compose(
        assocPath(['isLoading'], false),
        assocPath(['error'], result.error || null),
        assocPath(['formData', key, 'values'], result.error ? null : result)
      )(this.state)
    )
  }

  handleUpdate: HandleUpdateT = (newValue, name) => {
    let nextState

    // normalize input
    const value =
      newValue != null
        ? newValue.toLowerCase()
        : path(['formData', name, 'value'], this.state) || ''

    // validate input
    const valid = isVehicleKey(name)
      ? pathSatisfies(
          values => getCarOptionsByKey(name)(values).includes(value),
          ['formData', 'vehicle', 'values'],
          this.state
        )
      : pathSatisfies(
          values => values.includes(value),
          ['formData', name, 'values'],
          this.state
        )

    nextState = compose(
      assocPath(['isLoading'], valid && !isVehicleKey(name)),
      assocPath(['formData', name, 'value'], value),
      assocPath(['formData', name, 'valid'], valid)
      // $FlowIgnore compose returns StateT when evaluated
    )(this.state)

    // Needs re-fetching?
    if (!valid) {
      if (name === 'make') {
        // $FlowIgnore compose takes any mnumber of args
        nextState = compose(
          dissocPath(['formData', 'model', 'values']),
          dissocPath(['formData', 'model', 'valid']),
          dissocPath(['formData', 'vehicle', 'values']),
          dissocPath(['formData', 'vehicle', 'valid']),
          dissocPath(['formData', 'bodyType']),
          dissocPath(['formData', 'fuelType']),
          dissocPath(['formData', 'engineCapacity']),
          dissocPath(['formData', 'enginePowerKW'])
        )(nextState)
      }
      if (name === 'model') {
        nextState = compose(
          dissocPath(['formData', 'vehicle', 'values']),
          dissocPath(['formData', 'vehicle', 'valid']),
          dissocPath(['formData', 'bodyType']),
          dissocPath(['formData', 'fuelType']),
          dissocPath(['formData', 'engineCapacity']),
          dissocPath(['formData', 'enginePowerKW'])
        )(nextState)
      }
    }

    this.setState(nextState)
  }

  handleStepClick = (direction: 'back' | 'next') =>
    direction === 'next'
      ? () =>
          this.setState(prevState => ({
            step: prevState.step + 1,
          }))
      : () =>
          this.setState(prevState => ({
            step: prevState.step - 1,
          }))

  render() {
    const { error, isLoading, retries, step } = this.state
    const fieldHelper = makeFieldHelper(this.state, this.handleUpdate)

    const carsFound = getCarsFiltered(this.state)
    const score = getScore(this.state)

    return (
      <Fragment>
        <GitHubLink
          href="https://github.com/sombreroEnPuntas/car-gallery"
          target="_blank"
          rel="noopener noreferrer"
        >
          {'Check the code on github!'}
        </GitHubLink>
        <Form isLoading={isLoading} message={error} retry={retries < 3}>
          <Score>{`Score: ${('00' + score).slice(-3)}`}</Score>
          <FormSection active={step === 1} id="part-1">
            <p>{'Available makes:'}</p>
            <DropdownField {...fieldHelper('make')} />

            <p>{'Available models:'}</p>
            <DropdownField {...fieldHelper('model')} />
          </FormSection>
          <FormSection active={step === 2} id="part-2">
            <p>{'Available body types:'}</p>
            <SelectField {...fieldHelper('bodyType')} />

            <p>{'Available fuel types:'}</p>
            <SelectField {...fieldHelper('fuelType')} />
          </FormSection>
          <FormSection active={step === 3} id="part-3">
            <p>{'Available engine capacities:'}</p>
            <SelectField {...fieldHelper('engineCapacity')} />

            <p>{'Available engine power:'}</p>
            <SelectField {...fieldHelper('enginePowerKW')} />
          </FormSection>

          {/* // $FlowIgnore ramda.filter returns array when evaluated */}
          <p>{`Available cars: ${carsFound.length}`}</p>
          <FormButtons>
            {step !== 1 ? (
              <a data-test-id="back" onClick={this.handleStepClick('back')}>
                {'< Back'}
              </a>
            ) : (
              <div />
            )}
            {step !== 3 ? (
              <a data-test-id="next" onClick={this.handleStepClick('next')}>
                {'Next >'}
              </a>
            ) : (
              <div />
            )}
          </FormButtons>
        </Form>
      </Fragment>
    )
  }
}

export default Index