Now that we have a way to create categories, it’s time for handling bills.

We want to:

  1. have a way to enter a bill
  2. list all the bills

Let’s start with the first one. We’ll list bills in the next lesson.

Bills are stored in the App component, in its state bills property (an array). We already have the property in place.

Some design decisions I made:

  • for each bill, we’ll store an object into this array, with a date, a category name, and the amount.
  • the category is a string that represents the category name, not a reference to the category.
  • for convenience, all bills are expressed in $, but you can, of course, choose your own currency symbol.

Here’s an example bill object:

{
  date: '2018-01-30T15:08:26.118Z',
  amount: '220',
  category: 'Electricity'
}

I’m going to create an AddBill component, which will be very similar to the AddCategory component, except this time we have 2 more fields. One is a date picker, and another is a select that shows the categories list.

Let’s do it! First, let’s replicate the thing we have in the AddCategory component:

import React, { useState } from 'react'

export default props => {
  const [amount, setAmount] = useState(0)

  const handleChangeAmount = e => {
    setAmount(parseInt(e.target.value), 10)
  }

  const handleSubmit = e => {
    e.preventDefault()
    if (!amount) {
      alert('Please enter an amount')
      return
    }

    props.onSubmit(amount)
  }

  return (
    <form className="h-100 w-full flex items-center justify-center font-sans">
      <div className="bg-white rounded shadow p-6 m-4 w-full lg:w-3/4 lg:max-w-lg">
        <div className="mb-4">
          <h1 className="text-grey-darkest">Enter a new bill</h1>
          <p>E.g. 'Electricity' or 'Gas' or 'Internet'</p>
          <div className="flex mt-4">
            <input
              className="shadow appearance-none border rounded w-full py-2 px-3 mr-4 text-grey-darker"
              placeholder="Add category"
              value={amount}
              onChange={handleChangeAmount}
            />

            <button
              className="flex-no-shrink p-2 border-2 rounded bg-teal text-white border-teal hover:text-white hover:bg-teal"
              onClick={handleSubmit}
            >
              Add
            </button>
          </div>
        </div>
      </div>
    </form>
  )
}

Now, as mentioned we need a select that shows the categories. Those categories must be passed by the parent component App since AddBill does not have any notion of what categories are and what they are used for.

We pass the categories array as a prop:

<select>
  {props.categories
    ? props.categories.map((value, index) => {
        return (
          <option key={index} value={value}>
            {value}
          </option>
        )
      })
    : ''}
</select>

When the select changes we need to update the selected category. By default, it’s the first category passed in the categories prop:

//...
const [category, setCategory] = useState(props.categories[0])

//...
const handleChangeCategory = e => {
  setCategory(e.target.value)
}

Now it’s the turn of the last form element: the date picker.

Datepickers are hard to write, and so as a good lazy developer I searched “react datepicker” on Google, and I found this.

All we need to do is to add react-datepicker to the CodeSandbox dependencies, or if you are doing this project locally, run

npm install react-datepicker --save

or

yarn add react-datepicker

in the command line, from the root of your project.

Now we can import the react-datepicker component, and its CSS:

import DatePicker from 'react-datepicker'
import 'react-datepicker/dist/react-datepicker.css'

Then we add the state and change event handler:

const [date, setDate] = useState(new Date())
//...
const handleChangeDate = e => {
  setDate(e.target.value)
}

here’s the JSX:

<DatePicker selected={date} onChange={handleChangeDate} />

We’re almost done! Now we just need to implement the handleSubmit method, which is triggered when we press the Add button:

const handleSubmit = e => {
  e.preventDefault()
  if (!amount) {
    alert('Please enter an amount')
    return
  }

  props.onSubmit(amount, category || props.categories[0], date)
}

we check if the amount value is filled (date and category are always initialized to a first state, so there’s no need to check for their value), and we emit the onSubmit event to the parent, with as parameter the date, the category and the amount.

Using the or operator || we default to the first category if not set.

In the App component, we map onSubmit to the addBill function:

<AddBill onSubmit={addBill} categories={categories} />

Let’s implement this method:

const addBill = (amount, category, date) => {
  const bill = {amount, category, date}
  const updatedBills = [...(bills || []), bill]
  setBills(updatedBills)
  setShouldShowAddBill(false)
  localStorage.setItem('bills', JSON.stringify(updatedBills))
}

bills and setBills come from a new hook, which is same as we did for categories:

const [bills, setBills] = useState([])

setShouldShowAddBill is the same as setShouldShowAddCategory:

const [shouldShowAddBill, setShouldShowAddBill] = useState(true)

We repeat what we did for the categories to retrieve the bills from local storage in the useEffect() callback:

useEffect(() => {
  const categoriesInLocalStorage = JSON.parse(
    localStorage.getItem('categories')
  )
  const billsInLocalStorage = JSON.parse(localStorage.getItem('bills'))

  setCategories(categoriesInLocalStorage)
  setBills(billsInLocalStorage)

  if (!categoriesInLocalStorage) {
    setShouldShowAddCategory(true)
  }
}, [])

We only show this AddBill component if the shouldShowAddBill state property is true. And this will be handled later, but let’s add the logic to show it now, so we can test it.

We need to reorganize a bit the layout of the components in the App component. We only want to show this when shouldShowAddCategory is false, and we need to add another check for shouldShowAddBill:

function App() {
  //...
  return (
    <div className="App">
      {shouldShowAddCategory ? (
        <AddCategory onSubmit={addCategory} />
      ) : shouldShowAddBill ? (
        <AddBill onSubmit={addBill} categories={categories} />
      ) : (
        <div>
          <NavBar categories={categories} showAddCategory={showAddCategory} />
          <div className="container flex">
            <div className="w-1/2">
              <BillsTable />
            </div>
            <div className="w-1/2">
              <Chart />
            </div>
          </div>
        </div>
      )}
    </div>
  )
}

You should see it working now:

and if you try adding something and pressing “Add”, the view should show this:

See the app in the current state on CodeSandbox.


Go to the next lesson