Build an Appointment Scheduler Using React, Twilio and Cosmic JS

Written by tonyspiro | Published 2020/03/13
Tech Story Tags: javascript | api | cms | web-development | react | cosmicjs | nodejs | programming

TLDR Build an Appointment Scheduler Using React, Twilio and Cosmic JS to build an appointment scheduler. The app will let users select a day and a one-hour time slot between 9AM and 5PM to meet with us. We’ll use Cosmic JS as an API-first CMS, meaning it is language independent, database independent, and practically everything-else independent. We'll use two types of Objects to store our data: one for appointments and one for site configurations. Finally, we'll build a Cosmic JS Extension to manage the appointments right from within the Cosmic JS dashboard.via the TL;DR App

TL;DR

Your time is valuable but you can’t waste a second of it. People need to see you because work needs to get done and there are collaborations to be made. Instead of letting people communicate with you directly to schedule their use of your time — which only wastes it more — we’ll use Cosmic JS to build an appointment scheduler. That way, the people who need to talk to you only have to once.
Cosmic JS is an API-first CMS, meaning it is language independent, database independent, and practically everything-else independent. This is great for a small project like this one because we can extend it quickly with any language or framework in the future and we can define data structures that are only as complex as we need them to be.
Our Appointment Scheduler will let users select a day and a one-hour time slot between 9AM and 5PM to meet with us. We’ll then integrate our app with Twilio to send them a confirmation text that their appointment has been scheduled. Finally, we’ll build a Cosmic JS Extension so we can manage the appointments right from within the Cosmic JS dashboard.
(What our appointment scheduler app will look like)
We’ll complete our project in 3 major sections:
  • Building out the front-end in React and Webpack, with the help of the Material UI component library
  • Wiring it up to a simple Express backend that will serve to make API calls to Twilio and to expose our Appointment objects to the front-end (in the spirit of keeping our Cosmic bucket keys out of our frontend code)
  • Building the Extension in React, again using the Material UI library
But, before any of that, we need to get our Cosmic bucket ready to store and serve data.

Part 0: Setting Up Cosmic JS

We’ll use two types of Objects to store our data: one for appointments and one for site configurations. In Cosmic JS, first create the
Appointments
object type with the specified metafields. Then, we'll create a
Configs
object type with no default metafields and a Siteobject in which we'll define object-specific metafields.
Appointments
Here,
email
and
phone
will be the user's metadata — we'll use their name as the Appointment object's title.
Date 
will hold the appointment date in
YYYY-DD-MM
format and
slot
will be how many hours away the appointment is from 9AM.
Configs/Site
We’re using the
Config
object to define details about the app that we want to be able to change on the fly, rather than having to redeploy for.
And with our simple data scheme in place, we’re ready to get building.

Part 1: Building the Front End

1. Boilerplate setup

First, we’ll make our appointment-scheduler directory, run yarn init (feel free to use npm), and set up the project structure as follows:
appointment-scheduler
|
|--dist
|--src
|. |--Components
|. |. |--App.js
|. |--index.html
|. |--index.js
|--.babelrc
|--.gitignore
|--package.json
|--webpack.config.js
Then, we’ll make our HTML template:
<!-- ./src/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
    <title>Appointment Scheduler</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
Next, we’ll install the packages we need. Of interest, alongside any standard packages needed to develop a React app in ES6, we’ll be using:
  • async
    for an easy way to make ajax calls in series
  • axios
    as a useful ajax utility
  • babel-preset-stage-3
    to make use of the const { property } = object destructuring pattern
  • material-ui
    for a convenient set of React-built Material Design components
  • moment
    for parsing times
  • normalize.css
    to clear browser-default styles
  • react-tap-event-plugin
    - a necessary companion to
    material-ui
To install everything we need, run
yarn add async axios babel-preset-stage-3 material-ui moment normalze.css react react-dom react-tap-event-plugin
and for our dev-dependencies,
yarn add babel-core babel-loader babel-preset-env babel-preset-react css-loader eslint file-loader html-webpack-plugin path style-loader webpack
Having installed the Babel packages we need, we’ll tell Babel to use them in it’s config file and we’ll tell git to ignore everyhing we just installed:
  • // ./.babelrc { “presets”: [“env”, “react”, “stage-3”] }
  • # ./.gitignore node_modules
Finally, we’ll set up Webpack so everything’s in place for build time
const path = require(‘path’) const HtmlWebpackPlugin = require(‘html-webpack-plugin’) const webpack = require(‘webpack’) module.exports = { entry: ‘./src/index.js’, output: { path: path.resolve(‘dist’), filename: ‘bundle.js’, sourceMapFilename: ‘bundle.map.js’ }, devtool: ‘source-map’, devServer: { port: 8080 }, module: { rules: [{ test: /\.js$/, use: { loader: ‘babel-loader’ }, exclude: path.resolve(‘node_modules’) }, { test: [/\.scss$/,/\.css$/], loader: [‘style-loader’, ‘css-loader’, ‘sass-loader’] }, { test: /\.(png|jpg|gif|svg)$/, use: [ { loader: ‘file-loader’ } ] }] }, plugins: [ new HtmlWebpackPlugin({ template: ‘./src/index.html’, filename: ‘index.html’, inject: true, xhtml: true }), new webpack.DefinePlugin({ PRODUCTION: process.env.NODE_ENV === ‘production’ }) ] }
const path = require(‘path’) 
const HtmlWebpackPlugin = require(‘html-webpack-plugin’) 
const webpack = require(‘webpack’) 

module.exports = { 
	entry: ‘./src/index.js’, 
	output: {
	 	path: path.resolve(‘dist’), filename: ‘bundle.js’, sourceMapFilename: ‘bundle.map.js’ 
	 }, 
	 devtool: ‘source-map’, 
	 devServer: { port: 8080 }, 
	 module: { rules: [{ 
		 test: /\.js$/, use: { loader: ‘babel-loader’ }, 
		 exclude: path.resolve(‘node_modules’) 
		 }, { 
		 test: [/\.scss$/,/\.css$/], loader: [‘style-loader’, ‘css-loader’, ‘sass-loader’] 
		 }, {
		  test: /\.(png|jpg|gif|svg)$/, use: [ { loader: ‘file-loader’ } ] 
		 }] 
	}, 
	plugins: [ 
		new HtmlWebpackPlugin({ 
			template: ‘./src/index.html’, 
			filename: ‘index.html’, 
			inject: true, 
			xhtml: true 
		}), 
		new webpack.DefinePlugin({ 
			PRODUCTION: process.env.NODE_ENV === ‘production’ 
		}) 
	] 
}
We’ve configured Webpack to output
bundle.js
, it's source map, and
index.html
(according to the template in
src
) to
dist
on building. You also have the option to use SCSS throughout the project and have access to your Node environment via
window.PRODUCTION
at build-time.

2. Create an Entry Point

Before we go any further we need to define an entry point for our app. Here, we’ll import any necessary global libraries or wrapper components. We’ll also use it as the render point for our React app. This will be our
src/index.js
file and it will look like this:
// ./src/index.js
import React from 'react'
import ReactDom from 'react-dom'
import App from './Components/App'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import 'normalize.css'
require('./scss/app.scss')
window.React = React
ReactDom.render(
  <MuiThemeProvider>
    <App />
  </MuiThemeProvider>,
  document.getElementById('root')
)
// MuiThemeProvider is a wrapper component for MaterialUI's components

3. Work out a Skeleton

Having eveything in place to get our app working, we have a few things to consider and a few choices to make about how we want our app to work before we start building it. To help us think, we’ll build out a basic skeleton of how we need it to look:
// ./src/Components/App.js
import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
injectTapEventPlugin()
export default class App extends Component {
    constructor() {
    super()
      this.state = {
            // initial state
        }
        
  //method bindings
    }
  
  //component methods?
  
  //lifecycle methods
componentWillMount() {
        //fetch data from cosmic, watch window width
    }
  componentWillUnmount() {
        //remove window width event listener
    }
  render() {
     //define variables
      return (
        <div>
          </div>
        )
    }
}
To start, we need to think about what the app’s state will look like. Here are some considerations:
  • The app loads data from an external server, so it would be useful to show our user’s when that’s happening
  • Material Design implements a drawer-style navigation, so we need to track when that’s open
  • To confirm the user’s appointment details before submitting, we’ll show them a confirmation modal and for other notifactions we’ll use Material Design’s snackbar, which displays small notifications at the bottom of the page. We’ll need to track the open state of both of these.
  • Our appointment scheduling process we’ll take place in three steps: selecting a date, selecting a time slot, and filling out personal information. We need to track which step the user is on, the date and time they’ve selected, their contact details, and we also need to validate their email address and phone number.
  • We’ll be loading configuration data and a schedule appointments that we’d benefit from cacheing in the state.
  • As our user procedes through the 3 scheduling steps, we’ll show a friendly sentence tracking their progress. (Example: “Scheduling a 1 hour appointment at 3pm on…”). We need to know if that should be displayed.
  • When the user is selecting an appoitment slot, they’ll be able to filter by AM/PM, so we need to track which they’re looking for.
  • Finally, we’ll add some responsiveness to our styling and we need to keep track of the screen width.
We then arrive at this as our initial state:
// ./src/Components/App.js
// ...
this.state = {
  loading: true,
  navOpen: false,
  confirmationModalOpen: false,
  confirmationTextVisible: false,
  stepIndex: 0,
  appointmentDateSelected: false,
  appointmentMeridiem: 0,
  validEmail: false,
  validPhone: false,
  smallScreen: window.innerWidth < 768,
  confirmationSnackbarOpen: false
}
Note that
appointmentMeridiem
takes on
0
or
1
, such that
0 => 'AM'
and
1 => 'PM'
.

4. Draft Out Functionality

We’ve defined an initial state for our app, but before we build out a view with Material components we’ll find it useful to brainstorm what needs done with our data. Our app will boil down to the following functionality:
  • As decided in the previous step, our navitgation will be in a drawer so we need a
    handleNavToggle()
    method to display/hide it
  • The three scheduling steps are revealed to the user in succession upon completing a previous step, so we need a
    handleNextStep()
    method to handle the flow of user input
  • We’ll user a Material UI date picker to set our appointment date and we need a
    handleSetAppointmentDate()
    method to process data from that component. Likewise, we need a
    handleSetAppointmentSlot()
    and
    handleSetAppointmentMeridiem()
    method. We don't want the date picker to show unavailable days (including
    today
    ) so we need to pass a
    checkDisableDate()
    method to it.
  • In the
    componentWillMount()
    lifecycle method we'll fetch our data from our backend, then handle that data with a separate
    handleFetch()
    method. For a fetching error, we'll need a
    handleFetchError()
    method.
  • Upon submitting the appointment data, we’ll use
    handleSubmit()
    to send it to our backend. We'll need a
    validateEmail()
    and
    validatePhone()
    method for when the user is filling out contact information.
  • The user-friendly string above the form will be rendered in a separate method with
    renderConfirmationString()
    . So will available appointment times and the confirmation modal with
    renderAppointmentTimes()
    and
    renderAppointmentConfirmation()
    respectively.
  • Finally, we’ll use a handy
    resize()
    method to respond to the browser window changing in width
All in all, unwritten methods included, our
App.js
now looks like this (method bindings included):
// ./src/Components/App.js
import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
injectTapEventPlugin()
export default class App extends Component {
    constructor() {
    super()
      this.state = {
          loading: true,
          navOpen: false,
          confirmationModalOpen: false,
          confirmationTextVisible: false,
          stepIndex: 0,
          appointmentDateSelected: false,
          appointmentMeridiem: 0,
          validEmail: false,
          validPhone: false,
          smallScreen: window.innerWidth < 768,
          confirmationSnackbarOpen: false
        }
       
      //method bindings
      this.handleNavToggle = this.handleNavToggle.bind(this)
      this.handleNextStep = this.handleNextStep.bind(this)
      this.handleSetAppointmentDate = this.handleSetAppointmentDate.bind(this)
      this.handleSetAppointmentSlot = this.handleSetAppointmentSlot.bind(this)
      this.handleSetAppointmentMeridiem = this.handleSetAppointmentMeridiem.bind(this)
      this.handleSubmit = this.handleSubmit.bind(this)
      this.validateEmail = this.validateEmail.bind(this)
      this.validatePhone = this.validatePhone.bind(this)
      this.checkDisableDate = this.checkDisableDate.bind(this)
      this.renderAppointmentTimes = this.renderAppointmentTimes.bind(this)
      this.renderConfirmationString = this.renderConfirmationString.bind(this)
      this.renderAppointmentConfirmation = this.renderAppointmentConfirmation.bind(this)
      this.resize = this.resize.bind(this)
    }
  
  handleNavToggle() {
        
   }
  
  handleNextStep() {
        
    }
  
  handleSetAppointmentDate(date) {
        
    }
  
  handleSetAppointmentSlot(slot) {
        
    }
  
  handleSetAppointmentMeridiem(meridiem) {
        
    }
  
  handleFetch(response) {
        
    }
  
  handleFetchError(err) {
        
    }
  
  handleSubmit() {
        
    }
  
  validateEmail(email) {
        
    }
  
  validatePhone(phoneNumber) {
        
    }
  
  checkDisableDate(date) {
        
    }
  
  renderConfirmationString() {
        
    }
  
  renderAppointmentTimes() {
        
    }
  
  renderAppointmentConfirmation() {
        
    }
  
  resize() {
        
    }
  
  //lifecycle methods
componentWillMount() {
        //fetch data from cosmic, watch window width
    }
  componentWillUnmount() {
        //remove window width event listener
    }
  render() {
     //define variables
      return (
        <div>
          </div>
        )
    }
}

5. Build Out the View

Having a basic idea of how our app is going to function, we can start building out its UI. Besides a couple of wrappers and a few custom styles, the majority of our app will be constructed with pre-packaged Material UI components.
In order, we need:
  • An
    AppBar
    which acts as the primary toolbar
  • A
    Drawer
    , which is opened from the
    AppBar
    's primary button and serves as the app's navigation, following Material Design.
  • Within the
    Drawer
    ,
    MenuItem
    s to display links
  • A
    Card
    as the primary content container
  • A
    Stepper
    to break the scheduling process into 3 discreet steps. The active step will be expanded while the others are collapsed. The first step will be disabled if
    state.loading
    is true and the last two will be disabled as long as the user hasn't filled out the previous step.
  • Nested in the
    Stepper
    , three
    Steps
    which contain
    StepButton
    and
    StepContent
    components.
  • In the first
    Step
    , we'll use a
    DatePicker
    to let the user choose an appointment date. Unavailable days will be disabled according to the return value of
    checkDisableDate()
    . Selection of a date will be handled with
    handleSetAppointmentDate()
  • In the second
    Step
    , we want the user to be able to pick a time slot for their selected day from the slots available. We also want them to be able to filter times according to AM/PM. We'll use a
    SelectField
    for the filter and a
    RadioButtonGroup
    to hold the time slot buttons. We need extra logic to render the radio buttons, so will do that in the
    renderAppointmentTimes()
    method. That will return a set of
    RadioButton
    s.
  • The last
    Step
    will ask the user to input their name, email address, and phone number using
    TextField
    components. A
    RaisedButton
    will be used a submit button to open the confirmation
    Dialog
    . The users inputted phone number and email address will be validated with
    validatePhone()
    and
    validateEmail()
    respectively.
  • Finally, we’ll include a convenient
    SnackBar
    to display notifcations about the loading state and submission at the bottom of the page. All in all, after having written out the
    render()
    method, our app will look like this:
  • // ./src/Components/App.js
    // .. previous imports
    import AppBar from 'material-ui/AppBar'
    import Drawer from 'material-ui/Drawer'
    import Dialog from 'material-ui/Dialog'
    import Divider from 'material-ui/Divider'
    import MenuItem from 'material-ui/MenuItem'
    import Card from 'material-ui/Card'
    import DatePicker from 'material-ui/DatePicker'
    import TimePicker from 'material-ui/TimePicker'
    import TextField from 'material-ui/TextField'
    import SelectField from 'material-ui/SelectField'
    import SnackBar from 'material-ui/Snackbar'
    import {
      Step,
      Stepper,
      StepLabel,
      StepContent,
      StepButton
    } from 'material-ui/stepper'
    import {
      RadioButton,
      RadioButtonGroup
    } from 'material-ui/RadioButton'
    import RaisedButton from 'material-ui/RaisedButton';
    import FlatButton from 'material-ui/FlatButton'
    import logo from './../../dist/assets/logo.svg'
    export default class App extends Component {
        // ... component methods, lifecycle methods
      render() {
        const { stepIndex, loading, navOpen, smallScreen, confirmationModalOpen, confirmationSnackbarOpen, ...data } = this.state
        const contactFormFilled = data.firstName && data.lastName && data.phone && data.email && data.validPhone && data.validEmail
        const modalActions = [
          <FlatButton
            label="Cancel"
            primary={false}
            onClick={() => this.setState({ confirmationModalOpen : false})} />,
          <FlatButton
            label="Confirm"
            primary={true}
            onClick={() => this.handleSubmit()} />
        ]
        return (
          <div>
            <AppBar
              title={data.siteTitle}
              onLeftIconButtonTouchTap={() => this.handleNavToggle() }/>
            <Drawer
              docked={false}
              width={300}
              open={navOpen}
              onRequestChange={(navOpen) => this.setState({navOpen})} >
              <img src={logo}
                   style={{
                     height: 70,
                     marginTop: 50,
                     marginBottom: 30,
                     marginLeft: '50%',
                     transform: 'translateX(-50%)'
                   }}/>
              <a style={{textDecoration: 'none'}} href={this.state.homePageUrl}><MenuItem>Home</MenuItem></a>
              <a style={{textDecoration: 'none'}} href={this.state.aboutPageUrl}><MenuItem>About</MenuItem></a>
              <a style={{textDecoration: 'none'}} href={this.state.contactPageUrl}><MenuItem>Contact</MenuItem></a>
              <MenuItem disabled={true}
                        style={{
                          marginLeft: '50%',
                          transform: 'translate(-50%)'
                        }}>
                {"© Copyright " + moment().format('YYYY')}</MenuItem>
            </Drawer>
            <section style={{
                maxWidth: !smallScreen ? '80%' : '100%',
                margin: 'auto',
                marginTop: !smallScreen ? 20 : 0,
              }}>
              {this.renderConfirmationString()}
              <Card style={{
                  padding: '10px 10px 25px 10px',
                  height: smallScreen ? '100vh' : null
                }}>
                <Stepper
                  activeStep={stepIndex}
                  linear={false}
                  orientation="vertical">
                  <Step disabled={loading}>
                    <StepButton onClick={() => this.setState({ stepIndex: 0 })}>
                      Choose an available day for your appointment
                    </StepButton>
                    <StepContent>
                      <DatePicker
                          style={{
                            marginTop: 10,
                            marginLeft: 10
                          }}
                          value={data.appointmentDate}
                          hintText="Select a date"
                          mode={smallScreen ? 'portrait' : 'landscape'}
                          onChange={(n, date) => this.handleSetAppointmentDate(date)}
                          shouldDisableDate={day => this.checkDisableDate(day)}
                           />
                      </StepContent>
                  </Step>
                  <Step disabled={ !data.appointmentDate }>
                    <StepButton onClick={() => this.setState({ stepIndex: 1 })}>
                      Choose an available time for your appointment
                    </StepButton>
                    <StepContent>
                      <SelectField
                        floatingLabelText="AM or PM"
                        value={data.appointmentMeridiem}
                        onChange={(evt, key, payload) => this.handleSetAppointmentMeridiem(payload)}
                        selectionRenderer={value => value ? 'PM' : 'AM'}>
                        <MenuItem value={0}>AM</MenuItem>
                        <MenuItem value={1}>PM</MenuItem>
                      </SelectField>
                      <RadioButtonGroup
                        style={{ marginTop: 15,
                                 marginLeft: 15
                               }}
                        name="appointmentTimes"
                        defaultSelected={data.appointmentSlot}
                        onChange={(evt, val) => this.handleSetAppointmentSlot(val)}>
                        {this.renderAppointmentTimes()}
                      </RadioButtonGroup>
                    </StepContent>
                  </Step>
                  <Step disabled={ !Number.isInteger(this.state.appointmentSlot) }>
                    <StepButton onClick={() => this.setState({ stepIndex: 2 })}>
                      Share your contact information with us and we'll send you a reminder
                    </StepButton>
                    <StepContent>
                      <section>
                        <TextField
                          style={{ display: 'block' }}
                          name="first_name"
                          hintText="First Name"
                          floatingLabelText="First Name"
                          onChange={(evt, newValue) => this.setState({ firstName: newValue })}/>
                        <TextField
                          style={{ display: 'block' }}
                          name="last_name"
                          hintText="Last Name"
                          floatingLabelText="Last Name"
                          onChange={(evt, newValue) => this.setState({ lastName: newValue })}/>
                        <TextField
                          style={{ display: 'block' }}
                          name="email"
                          hintText="name@mail.com"
                          floatingLabelText="Email"
                          errorText={data.validEmail ? null : 'Enter a valid email address'}
                          onChange={(evt, newValue) => this.validateEmail(newValue)}/>
                        <TextField
                          style={{ display: 'block' }}
                          name="phone"
                          hintText="(888) 888-8888"
                          floatingLabelText="Phone"
                          errorText={data.validPhone ? null: 'Enter a valid phone number'}
                          onChange={(evt, newValue) => this.validatePhone(newValue)} />
                        <RaisedButton
                          style={{ display: 'block' }}
                          label={contactFormFilled ? 'Schedule' : 'Fill out your information to schedule'}
                          labelPosition="before"
                          primary={true}
                          fullWidth={true}
                          onClick={() => this.setState({ confirmationModalOpen: !this.state.confirmationModalOpen })}
                          disabled={!contactFormFilled || data.processed }
                          style={{ marginTop: 20, maxWidth: 100}} />
                      </section>
                    </StepContent>
                  </Step>
                </Stepper>
              </Card>
              <Dialog
                modal={true}
                open={confirmationModalOpen}
                actions={modalActions}
                title="Confirm your appointment">
                {this.renderAppointmentConfirmation()}
              </Dialog>
              <SnackBar
                open={confirmationSnackbarOpen || loading}
                message={loading ? 'Loading... ' : data.confirmationSnackbarMessage || ''}
                autoHideDuration={10000}
                onRequestClose={() => this.setState({ confirmationSnackbarOpen: false })} />
            </section>
          </div>
        )
      }
    }
Before moving on, notice that, because they take some extra logic to render, we’ll be handing the radio buttons for the time slots and the confirmation strings in their own methods.
With the view components in place, our last big step will be to write all of the functionality we’ve mapped out.

6. Component Lifecycle Methods

componentWillMount()
The first step in adding functionality will be to write out the
componentWillMount()
method. In
componentWillMount()
we'll use
axios
to fetch our configuration and appointments data from our backend. Again, we're using our backend as a middleman so we can selectively expose data to our front end and omit things like users' contact information.
// ./src/Components/App.js
// previous imports
import async from 'async'
import axios from 'axios'
export default class App extends Component {
constructor() {}
  
  componentWillMount() {
    async.series({
        configs(callback) {
          axios.get(HOST + 'api/config').then(res =>
            callback(null, res.data.data)
          )
        },
        appointments(callback) {
          axios.get(HOST + 'api/appointments').then(res => {
            callback(null, res.data.data)
          })
       }
      }, (err,response) => {
        err ? this.handleFetchError(err) : this.handleFetch(response)
    })
    addEventListener('resize', this.resize)
    }
  
 // rest...
}
We use
async
to make our
axios
calls in series, and name them so we have access to them as
response.configs
and
response.appointments
in
handleFetch()
. We also use
componentWillMount
to start tracking the window width with
resize()
.
componentWillUnmount()
Practicing good form, we’ll remove the event listener in
componentWillUnmount()
.
// ./src/Components/App.js
// previous imports
import async from 'async'
import axios from 'axios'
export default class App extends Component {
constructor() {}
  
  componentWillUnmount() {
removeEventListener('resize', this.resize)
    }
  
 // rest...
}

7. Processing Data

Having fetched the data we need, we’ll process a successful fetch with
handleFetch()
and an error with
handleFetchError()
. In
handleFetch()
we'll build a schedule of appointmens to store in the state, such that
schedule = { appointmentDate: [slots] }
. We also use this method to store the app's configuration data in the state.
handleFetch()
handleFetch(response) {
const { configs, appointments } = response
    const initSchedule = {}
    const today = moment().startOf('day')
    initSchedule[today.format('YYYY-DD-MM')] = true
    const schedule = !appointments.length ? initSchedule : appointments.reduce((currentSchedule, appointment) => {
      const { date, slot } = appointment
      const dateString = moment(date, 'YYYY-DD-MM').format('YYYY-DD-MM')
      !currentSchedule[date] ? currentSchedule[dateString] = Array(8).fill(false) : null
      Array.isArray(currentSchedule[dateString]) ?
        currentSchedule[dateString][slot] = true : null
      return currentSchedule
    }, initSchedule)
    for (let day in schedule) {
      let slots = schedule[day]
      slots.length ? (slots.every(slot => slot === true)) ? schedule[day] = true : null : null
    }
    this.setState({
      schedule,
      siteTitle: configs.site_title,
      aboutPageUrl: configs.about_page_url,
      contactPageUrl: configs.contact_page_url,
      homePageUrl: configs.home_page_url,
      loading: false
    })
  }
handleFetchError()
For handling errors, we’ll simply show the users an error message in the
SnackBar
.
handleFetchError(err) {
console.log('Error fetching data:' + err)
    this.setState({ confirmationSnackbarMessage: 'Error fetching data', confirmationSnackbarOpen: true })
}

8. Handle UI Changes

We need to manage the state whenever the user opens the Drawer, moves onto another step, or if the browser width changes. First we'll handle the drawer toggle.
handleNavToggle() {
  return this.setState({ navOpen: !this.state.navOpen })
}
Then, as long as the user isn’t on the last step, we’ll handle incrementing the step.
handleNextStep() {
  const { stepIndex } = this.state
  return (stepIndex < 3) ? this.setState({ stepIndex: stepIndex + 1}) : null
}
Finally, we’ll simply change the state on resize if the window width is less than 768px.
resize() {
  this.setState({ smallScreen: window.innerWidth < 768 })
}

9. Handle Setting Appointment Data

When the user selects appointment options in steps one and two, we need three simple setters to change the state to reflect those selections.
handleSetAppointmentDate(date) {
  this.handleNextStep()
  this.setState({ appointmentDate: date, confirmationTextVisible: true })
}
handleSetAppointmentSlot(slot) {
  this.handleNextStep()
  this.setState({ appointmentSlot: slot })
}
handleSetAppointmentMeridiem(meridiem) {
  this.setState({ appointmentMeridiem: meridiem})
}

10. Handle Validations

We need to validate the user’s inputted email address, phone number, and we need to feed the
DatePicker 
component a function to check which days should be disabled. Although naive, we'll be using regex's to check the inputs for simplicities sake.
validateEmail(email) {
  const regex = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
  return regex.test(email) ? this.setState({ email: email, validEmail: true }) : this.setState({ validEmail: false })
}
validatePhone(phoneNumber) {
  const regex = /^(1\s|1|)?((\(\d{3}\))|\d{3})(\-|\s)?(\d{3})(\-|\s)?(\d{4})$/
  return regex.test(phoneNumber) ? this.setState({ phone: phoneNumber, validPhone: true }) : this.setState({ validPhone: false })
}
For checking if a date should be disabled, we need to check if the date passed by
DatePicker
is either in
state.schedule
or is today.
checkDisableDate(day) {
    const dateString = moment(day).format('YYYY-DD-MM')
    return this.state.schedule[dateString] === true || moment(day).startOf('day').diff(moment().startOf('day')) < 0
  }

11. Build the Render Methods for the confirmation strings and the time slot radio buttons

In our
render()
lifecycle method we abstracted out the logic for displaying the dynamic confirmation string above the
Card
, the confirmation details we'll show in the confirmation modal, and the radio buttons for selecting a time slot.
Starting with the confirmation string, we’ll display the parts of it that correspond to inputted data only as it’s entered.
renderConfirmationString() {
  const spanStyle = {color: '#00bcd4'}
  return this.state.confirmationTextVisible ? <h2 style={{ textAlign: this.state.smallScreen ? 'center' : 'left', color: '#bdbdbd', lineHeight: 1.5, padding: '0 10px', fontFamily: 'Roboto'}}>
    { <span>
     Scheduling a
     <span style={spanStyle}> 1 hour </span>
appointment {this.state.appointmentDate && <span>
  on <span style={spanStyle}>{moment(this.state.appointmentDate).format('dddd[,] MMMM Do')}</span>
             </span>} {Number.isInteger(this.state.appointmentSlot) && <span>at <span style={spanStyle}>{moment().hour(9).minute(0).add(this.state.appointmentSlot, 'hours').format('h:mm a')}</span></span>}
             </span>}
             </h2> : null
}
Then, similarly, we’ll let the user verify their data before confirming submission.
renderAppointmentConfirmation() {
  const spanStyle = { color: '#00bcd4' }
  return <section>
    <p>Name: <span style={spanStyle}>{this.state.firstName} {this.state.lastName}</span></p>
      <p>Number: <span style={spanStyle}>{this.state.phone}</span></p>
        <p>Email: <span style={spanStyle}>{this.state.email}</span></p>
          <p>Appointment: <span style={spanStyle}>{moment(this.state.appointmentDate).format('dddd[,] MMMM Do[,] YYYY')}</span> at <span style={spanStyle}>{moment().hour(9).minute(0).add(this.state.appointmentSlot, 'hours').format('h:mm a')}</span></p>
  </section>
}
Finally, we’ll write the method to render the appointment slot radio buttons. To do this we first have to filter the slots by availabity and by whether AM or PM is selected. Both are simple checks; for the first we see if it exists in
state.schedule
, for the later, we check the meridiem part with
moment().format('a')
. To compute the time string in 12 hour format, we add the slot to 9AM in units of hours.
renderAppointmentTimes() {
  if (!this.state.loading) {
    const slots = [...Array(8).keys()]
    return slots.map(slot => {
      const appointmentDateString = moment(this.state.appointmentDate).format('YYYY-DD-MM')
      const t1 = moment().hour(9).minute(0).add(slot, 'hours')
      const t2 = moment().hour(9).minute(0).add(slot + 1, 'hours')
      const scheduleDisabled = this.state.schedule[appointmentDateString] ? this.state.schedule[moment(this.state.appointmentDate).format('YYYY-DD-MM')][slot] : false
      const meridiemDisabled = this.state.appointmentMeridiem ? t1.format('a') === 'am' : t1.format('a') === 'pm'
      return <RadioButton
      label={t1.format('h:mm a') + ' - ' + t2.format('h:mm a')}
      key={slot}
      value={slot}
      style={{marginBottom: 15, display: meridiemDisabled ? 'none' : 'inherit'}}
                     disabled={scheduleDisabled || meridiemDisabled}/>
                     })
  } else {
    return null
  }
}

13. Handle the Form Submission

Once we show the user the confirmation modal, upon final submission we’ll send the data to our backend with an
axios
POST. We'll notifty them of either a success or an error.
handleSubmit() {
    const appointment = {
      date: moment(this.state.appointmentDate).format('YYYY-DD-MM'),
      slot: this.state.appointmentSlot,
      name: this.state.firstName + ' ' + this.state.lastName,
      email: this.state.email,
      phone: this.state.phone
    }
    axios.post(HOST + 'api/appointments', )
    axios.post(HOST + 'api/appointments', appointment)
    .then(response => this.setState({ confirmationSnackbarMessage: "Appointment succesfully added!", confirmationSnackbarOpen: true, processed: true }))
    .catch(err => {
      console.log(err)
      return this.setState({ confirmationSnackbarMessage: "Appointment failed to save.", confirmationSnackbarOpen: true })
    })
  }

14. Conclusion: Seeing it All Together

Before moving on to building the backend, here’s what we have as our final product.
// ./src/Components/App.js
import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
import axios from 'axios'
import async from 'async'
import moment from 'moment'
import AppBar from 'material-ui/AppBar'
import Drawer from 'material-ui/Drawer'
import Dialog from 'material-ui/Dialog'
import Divider from 'material-ui/Divider'
import MenuItem from 'material-ui/MenuItem'
import Card from 'material-ui/Card'
import DatePicker from 'material-ui/DatePicker'
import TimePicker from 'material-ui/TimePicker'
import TextField from 'material-ui/TextField'
import SelectField from 'material-ui/SelectField'
import SnackBar from 'material-ui/Snackbar'
import {
  Step,
  Stepper,
  StepLabel,
  StepContent,
  StepButton
} from 'material-ui/stepper'
import {
  RadioButton,
  RadioButtonGroup
} from 'material-ui/RadioButton'
import RaisedButton from 'material-ui/RaisedButton';
import FlatButton from 'material-ui/FlatButton'
import logo from './../../dist/assets/logo.svg'
injectTapEventPlugin()
const HOST = PRODUCTION ? '/' : 'http://localhost:3000/'
export default class App extends Component {
  constructor() {
    super()
    this.state = {
      loading: true,
      navOpen: false,
      confirmationModalOpen: false,
      confirmationTextVisible: false,
      stepIndex: 0,
      appointmentDateSelected: false,
      appointmentMeridiem: 0,
      validEmail: true,
      validPhone: true,
      smallScreen: window.innerWidth < 768,
      confirmationSnackbarOpen: false
    }
    this.handleNavToggle = this.handleNavToggle.bind(this)
    this.handleNextStep = this.handleNextStep.bind(this)
    this.handleSetAppointmentDate = this.handleSetAppointmentDate.bind(this)
    this.handleSetAppointmentSlot = this.handleSetAppointmentSlot.bind(this)
    this.handleSetAppointmentMeridiem = this.handleSetAppointmentMeridiem.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
    this.validateEmail = this.validateEmail.bind(this)
    this.validatePhone = this.validatePhone.bind(this)
    this.checkDisableDate = this.checkDisableDate.bind(this)
    this.renderAppointmentTimes = this.renderAppointmentTimes.bind(this)
    this.renderConfirmationString = this.renderConfirmationString.bind(this)
    this.renderAppointmentConfirmation = this.renderAppointmentConfirmation.bind(this)
    this.resize = this.resize.bind(this)
  }
  handleNavToggle() {
    return this.setState({ navOpen: !this.state.navOpen })
  }
  handleNextStep() {
    const { stepIndex } = this.state
    return (stepIndex < 3) ? this.setState({ stepIndex: stepIndex + 1}) : null
  }
  handleSetAppointmentDate(date) {
    this.handleNextStep()
    this.setState({ appointmentDate: date, confirmationTextVisible: true })
  }
  handleSetAppointmentSlot(slot) {
    this.handleNextStep()
    this.setState({ appointmentSlot: slot })
  }
  handleSetAppointmentMeridiem(meridiem) {
    this.setState({ appointmentMeridiem: meridiem})
  }
  handleFetch(response) {
    const { configs, appointments } = response
    const initSchedule = {}
    const today = moment().startOf('day')
    initSchedule[today.format('YYYY-DD-MM')] = true
    const schedule = !appointments.length ? initSchedule : appointments.reduce((currentSchedule, appointment) => {
      const { date, slot } = appointment
      const dateString = moment(date, 'YYYY-DD-MM').format('YYYY-DD-MM')
      !currentSchedule[date] ? currentSchedule[dateString] = Array(8).fill(false) : null
      Array.isArray(currentSchedule[dateString]) ?
        currentSchedule[dateString][slot] = true : null
      return currentSchedule
    }, initSchedule)
    for (let day in schedule) {
      let slots = schedule[day]
      slots.length ? (slots.every(slot => slot === true)) ? schedule[day] = true : null : null
    }
    this.setState({
      schedule,
      siteTitle: configs.site_title,
      aboutPageUrl: configs.about_page_url,
      contactPageUrl: configs.contact_page_url,
      homePageUrl: configs.home_page_url,
      loading: false
    })
  }
  handleFetchError(err) {
    console.log('Error fetching data:' + err)
    this.setState({ confirmationSnackbarMessage: 'Error fetching data', confirmationSnackbarOpen: true })
  }
  handleSubmit() {
    const appointment = {
      date: moment(this.state.appointmentDate).format('YYYY-DD-MM'),
      slot: this.state.appointmentSlot,
      name: this.state.firstName + ' ' + this.state.lastName,
      email: this.state.email,
      phone: this.state.phone
    }
    axios.post(HOST + 'api/appointments', )
    axios.post(HOST + 'api/appointments', appointment)
    .then(response => this.setState({ confirmationSnackbarMessage: "Appointment succesfully added!", confirmationSnackbarOpen: true, processed: true }))
    .catch(err => {
      console.log(err)
      return this.setState({ confirmationSnackbarMessage: "Appointment failed to save.", confirmationSnackbarOpen: true })
    })
  }
  validateEmail(email) {
    const regex = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
    return regex.test(email) ? this.setState({ email: email, validEmail: true }) : this.setState({ validEmail: false })
  }
  validatePhone(phoneNumber) {
    const regex = /^(1\s|1|)?((\(\d{3}\))|\d{3})(\-|\s)?(\d{3})(\-|\s)?(\d{4})$/
    return regex.test(phoneNumber) ? this.setState({ phone: phoneNumber, validPhone: true }) : this.setState({ validPhone: false })
  }
  checkDisableDate(day) {
    const dateString = moment(day).format('YYYY-DD-MM')
    return this.state.schedule[dateString] === true || moment(day).startOf('day').diff(moment().startOf('day')) < 0
  }
  renderConfirmationString() {
    const spanStyle = {color: '#00bcd4'}
    return this.state.confirmationTextVisible ? <h2 style={{ textAlign: this.state.smallScreen ? 'center' : 'left', color: '#bdbdbd', lineHeight: 1.5, padding: '0 10px', fontFamily: 'Roboto'}}>
      { <span>
        Scheduling a
          <span style={spanStyle}> 1 hour </span>
        appointment {this.state.appointmentDate && <span>
          on <span style={spanStyle}>{moment(this.state.appointmentDate).format('dddd[,] MMMM Do')}</span>
      </span>} {Number.isInteger(this.state.appointmentSlot) && <span>at <span style={spanStyle}>{moment().hour(9).minute(0).add(this.state.appointmentSlot, 'hours').format('h:mm a')}</span></span>}
      </span>}
    </h2> : null
  }
  renderAppointmentTimes() {
    if (!this.state.loading) {
      const slots = [...Array(8).keys()]
      return slots.map(slot => {
        const appointmentDateString = moment(this.state.appointmentDate).format('YYYY-DD-MM')
        const t1 = moment().hour(9).minute(0).add(slot, 'hours')
        const t2 = moment().hour(9).minute(0).add(slot + 1, 'hours')
        const scheduleDisabled = this.state.schedule[appointmentDateString] ? this.state.schedule[moment(this.state.appointmentDate).format('YYYY-DD-MM')][slot] : false
        const meridiemDisabled = this.state.appointmentMeridiem ? t1.format('a') === 'am' : t1.format('a') === 'pm'
        return <RadioButton
          label={t1.format('h:mm a') + ' - ' + t2.format('h:mm a')}
          key={slot}
          value={slot}
          style={{marginBottom: 15, display: meridiemDisabled ? 'none' : 'inherit'}}
          disabled={scheduleDisabled || meridiemDisabled}/>
      })
    } else {
      return null
    }
  }
  renderAppointmentConfirmation() {
    const spanStyle = { color: '#00bcd4' }
    return <section>
      <p>Name: <span style={spanStyle}>{this.state.firstName} {this.state.lastName}</span></p>
      <p>Number: <span style={spanStyle}>{this.state.phone}</span></p>
      <p>Email: <span style={spanStyle}>{this.state.email}</span></p>
      <p>Appointment: <span style={spanStyle}>{moment(this.state.appointmentDate).format('dddd[,] MMMM Do[,] YYYY')}</span> at <span style={spanStyle}>{moment().hour(9).minute(0).add(this.state.appointmentSlot, 'hours').format('h:mm a')}</span></p>
    </section>
  }
  resize() {
    this.setState({ smallScreen: window.innerWidth < 768 })
  }
  componentWillMount() {
    async.series({
      configs(callback) {
        axios.get(HOST + 'api/config').then(res =>
          callback(null, res.data.data)
        )
      },
      appointments(callback) {
        axios.get(HOST + 'api/appointments').then(res => {
          callback(null, res.data.data)
        })
      }
    }, (err,response) => {
      err ? this.handleFetchError(err) : this.handleFetch(response)
    })
    addEventListener('resize', this.resize)
  }
  componentWillUnmount() {
    removeEventListener('resize', this.resize)
  }
  render() {
    const { stepIndex, loading, navOpen, smallScreen, confirmationModalOpen, confirmationSnackbarOpen, ...data } = this.state
    const contactFormFilled = data.firstName && data.lastName && data.phone && data.email && data.validPhone && data.validEmail
    const modalActions = [
      <FlatButton
        label="Cancel"
        primary={false}
        onClick={() => this.setState({ confirmationModalOpen : false})} />,
      <FlatButton
        label="Confirm"
        primary={true}
        onClick={() => this.handleSubmit()} />
    ]
    return (
      <div>
        <AppBar
          title={data.siteTitle}
          onLeftIconButtonTouchTap={() => this.handleNavToggle() }/>
        <Drawer
          docked={false}
          width={300}
          open={navOpen}
          onRequestChange={(navOpen) => this.setState({navOpen})} >
          <img src={logo}
               style={{
                 height: 70,
                 marginTop: 50,
                 marginBottom: 30,
                 marginLeft: '50%',
                 transform: 'translateX(-50%)'
               }}/>
          <a style={{textDecoration: 'none'}} href={this.state.homePageUrl}><MenuItem>Home</MenuItem></a>
          <a style={{textDecoration: 'none'}} href={this.state.aboutPageUrl}><MenuItem>About</MenuItem></a>
          <a style={{textDecoration: 'none'}} href={this.state.contactPageUrl}><MenuItem>Contact</MenuItem></a>
          <MenuItem disabled={true}
                    style={{
                      marginLeft: '50%',
                      transform: 'translate(-50%)'
                    }}>
            {"© Copyright " + moment().format('YYYY')}</MenuItem>
        </Drawer>
        <section style={{
            maxWidth: !smallScreen ? '80%' : '100%',
            margin: 'auto',
            marginTop: !smallScreen ? 20 : 0,
          }}>
          {this.renderConfirmationString()}
          <Card style={{
              padding: '10px 10px 25px 10px',
              height: smallScreen ? '100vh' : null
            }}>
            <Stepper
              activeStep={stepIndex}
              linear={false}
              orientation="vertical">
              <Step disabled={loading}>
                <StepButton onClick={() => this.setState({ stepIndex: 0 })}>
                  Choose an available day for your appointment
                </StepButton>
                <StepContent>
                  <DatePicker
                      style={{
                        marginTop: 10,
                        marginLeft: 10
                      }}
                      value={data.appointmentDate}
                      hintText="Select a date"
                      mode={smallScreen ? 'portrait' : 'landscape'}
                      onChange={(n, date) => this.handleSetAppointmentDate(date)}
                      shouldDisableDate={day => this.checkDisableDate(day)}
                       />
                  </StepContent>
              </Step>
              <Step disabled={ !data.appointmentDate }>
                <StepButton onClick={() => this.setState({ stepIndex: 1 })}>
                  Choose an available time for your appointment
                </StepButton>
                <StepContent>
                  <SelectField
                    floatingLabelText="AM or PM"
                    value={data.appointmentMeridiem}
                    onChange={(evt, key, payload) => this.handleSetAppointmentMeridiem(payload)}
                    selectionRenderer={value => value ? 'PM' : 'AM'}>
                    <MenuItem value={0}>AM</MenuItem>
                    <MenuItem value={1}>PM</MenuItem>
                  </SelectField>
                  <RadioButtonGroup
                    style={{ marginTop: 15,
                             marginLeft: 15
                           }}
                    name="appointmentTimes"
                    defaultSelected={data.appointmentSlot}
                    onChange={(evt, val) => this.handleSetAppointmentSlot(val)}>
                    {this.renderAppointmentTimes()}
                  </RadioButtonGroup>
                </StepContent>
              </Step>
              <Step disabled={ !Number.isInteger(this.state.appointmentSlot) }>
                <StepButton onClick={() => this.setState({ stepIndex: 2 })}>
                  Share your contact information with us and we'll send you a reminder
                </StepButton>
                <StepContent>
                  <section>
                    <TextField
                      style={{ display: 'block' }}
                      name="first_name"
                      hintText="First Name"
                      floatingLabelText="First Name"
                      onChange={(evt, newValue) => this.setState({ firstName: newValue })}/>
                    <TextField
                      style={{ display: 'block' }}
                      name="last_name"
                      hintText="Last Name"
                      floatingLabelText="Last Name"
                      onChange={(evt, newValue) => this.setState({ lastName: newValue })}/>
                    <TextField
                      style={{ display: 'block' }}
                      name="email"
                      hintText="name@mail.com"
                      floatingLabelText="Email"
                      errorText={data.validEmail ? null : 'Enter a valid email address'}
                      onChange={(evt, newValue) => this.validateEmail(newValue)}/>
                    <TextField
                      style={{ display: 'block' }}
                      name="phone"
                      hintText="(888) 888-8888"
                      floatingLabelText="Phone"
                      errorText={data.validPhone ? null: 'Enter a valid phone number'}
                      onChange={(evt, newValue) => this.validatePhone(newValue)} />
                    <RaisedButton
                      style={{ display: 'block' }}
                      label={contactFormFilled ? 'Schedule' : 'Fill out your information to schedule'}
                      labelPosition="before"
                      primary={true}
                      fullWidth={true}
                      onClick={() => this.setState({ confirmationModalOpen: !this.state.confirmationModalOpen })}
                      disabled={!contactFormFilled || data.processed }
                      style={{ marginTop: 20, maxWidth: 100}} />
                  </section>
                </StepContent>
              </Step>
            </Stepper>
          </Card>
          <Dialog
            modal={true}
            open={confirmationModalOpen}
            actions={modalActions}
            title="Confirm your appointment">
            {this.renderAppointmentConfirmation()}
          </Dialog>
          <SnackBar
            open={confirmationSnackbarOpen || loading}
            message={loading ? 'Loading... ' : data.confirmationSnackbarMessage || ''}
            autoHideDuration={10000}
            onRequestClose={() => this.setState({ confirmationSnackbarOpen: false })} />
        </section>
      </div>
    )
  }
}
<code class="language-markup">import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
import axios from 'axios'
import async from 'async'
import moment from 'moment'
import AppBar from 'material-ui/AppBar'
import Drawer from 'material-ui/Drawer'
import Dialog from 'material-ui/Dialog'
import Divider from 'material-ui/Divider'
import MenuItem from 'material-ui/MenuItem'
import Card from 'material-ui/Card'
import DatePicker from 'material-ui/DatePicker'
import TimePicker from 'material-ui/TimePicker'
import TextField from 'material-ui/TextField'
import SelectField from 'material-ui/SelectField'
import SnackBar from 'material-ui/Snackbar'
import {
  Step,
  Stepper,
  StepLabel,
  StepContent,
  StepButton
} from 'material-ui/stepper'
import {
  RadioButton,
  RadioButtonGroup
} from 'material-ui/RadioButton'
import RaisedButton from 'material-ui/RaisedButton';
import FlatButton from 'material-ui/FlatButton'
import logo from './../../dist/assets/logo.svg'</code>
<code class="language-markup">injectTapEventPlugin()
const HOST = PRODUCTION ? '/' : 'http://localhost:3000/'</code>
<code class="language-markup">export default class App extends Component {
  constructor() {
    super()
    this.state = {
      loading: true,
      navOpen: false,
      confirmationModalOpen: false,
      confirmationTextVisible: false,
      stepIndex: 0,
      appointmentDateSelected: false,
      appointmentMeridiem: 0,
      validEmail: true,
      validPhone: true,
      smallScreen: window.innerWidth &lt; 768,
      confirmationSnackbarOpen: false
    }</code>
<code class="language-markup">    this.handleNavToggle = this.handleNavToggle.bind(this)
    this.handleNextStep = this.handleNextStep.bind(this)
    this.handleSetAppointmentDate = this.handleSetAppointmentDate.bind(this)
    this.handleSetAppointmentSlot = this.handleSetAppointmentSlot.bind(this)
    this.handleSetAppointmentMeridiem = this.handleSetAppointmentMeridiem.bind(this)
    this.handleSubmit = this.handleSubmit.bind(this)
    this.validateEmail = this.validateEmail.bind(this)
    this.validatePhone = this.validatePhone.bind(this)
    this.checkDisableDate = this.checkDisableDate.bind(this)
    this.renderAppointmentTimes = this.renderAppointmentTimes.bind(this)
    this.renderConfirmationString = this.renderConfirmationString.bind(this)
    this.renderAppointmentConfirmation = this.renderAppointmentConfirmation.bind(this)
    this.resize = this.resize.bind(this)
  }</code>
<code class="language-markup">  handleNavToggle() {
    return this.setState({ navOpen: !this.state.navOpen })
  }</code>
<code class="language-markup">  handleNextStep() {
    const { stepIndex } = this.state
    return (stepIndex &lt; 3) ? this.setState({ stepIndex: stepIndex + 1}) : null
  }</code>
<code class="language-markup">  handleSetAppointmentDate(date) {
    this.handleNextStep()
    this.setState({ appointmentDate: date, confirmationTextVisible: true })
  }</code>
<code class="language-markup">  handleSetAppointmentSlot(slot) {
    this.handleNextStep()
    this.setState({ appointmentSlot: slot })
  }</code>
<code class="language-markup">  handleSetAppointmentMeridiem(meridiem) {
    this.setState({ appointmentMeridiem: meridiem})
  }</code>
<code class="language-markup">  handleFetch(response) {
    const { configs, appointments } = response
    const initSchedule = {}
    const today = moment().startOf('day')
    initSchedule[today.format('YYYY-DD-MM')] = true
    const schedule = !appointments.length ? initSchedule : appointments.reduce((currentSchedule, appointment) =&gt; {
      const { date, slot } = appointment
      const dateString = moment(date, 'YYYY-DD-MM').format('YYYY-DD-MM')
      !currentSchedule[date] ? currentSchedule[dateString] = Array(8).fill(false) : null
      Array.isArray(currentSchedule[dateString]) ?
        currentSchedule[dateString][slot] = true : null
      return currentSchedule
    }, initSchedule)</code>
<code class="language-markup">    for (let day in schedule) {
      let slots = schedule[day]
      slots.length ? (slots.every(slot =&gt; slot === true)) ? schedule[day] = true : null : null
    }</code>
<code class="language-markup">    this.setState({
      schedule,
      siteTitle: configs.site_title,
      aboutPageUrl: configs.about_page_url,
      contactPageUrl: configs.contact_page_url,
      homePageUrl: configs.home_page_url,
      loading: false
    })
  }</code>
<code class="language-markup">  handleFetchError(err) {
    console.log('Error fetching data:' + err)
    this.setState({ confirmationSnackbarMessage: 'Error fetching data', confirmationSnackbarOpen: true })
  }</code>
<code class="language-markup">  handleSubmit() {
    const appointment = {
      date: moment(this.state.appointmentDate).format('YYYY-DD-MM'),
      slot: this.state.appointmentSlot,
      name: this.state.firstName + ' ' + this.state.lastName,
      email: this.state.email,
      phone: this.state.phone
    }
    axios.post(HOST + 'api/appointments', )
    axios.post(HOST + 'api/appointments', appointment)
    .then(response =&gt; this.setState({ confirmationSnackbarMessage: "Appointment succesfully added!", confirmationSnackbarOpen: true, processed: true }))
    .catch(err =&gt; {
      console.log(err)
      return this.setState({ confirmationSnackbarMessage: "Appointment failed to save.", confirmationSnackbarOpen: true })
    })
  }</code>
<code class="language-markup">  validateEmail(email) {
    const regex = /^(([^&lt;&gt;()\[\]\.,;:\s@\"]+(\.[^&lt;&gt;()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^&lt;&gt;()[\]\.,;:\s@\"]+\.)+[^&lt;&gt;()[\]\.,;:\s@\"]{2,})$/i
    return regex.test(email) ? this.setState({ email: email, validEmail: true }) : this.setState({ validEmail: false })
  }</code>
<code class="language-markup">  validatePhone(phoneNumber) {
    const regex = /^(1\s|1|)?((\(\d{3}\))|\d{3})(\-|\s)?(\d{3})(\-|\s)?(\d{4})$/
    return regex.test(phoneNumber) ? this.setState({ phone: phoneNumber, validPhone: true }) : this.setState({ validPhone: false })
  }</code>
<code class="language-markup">  checkDisableDate(day) {
    const dateString = moment(day).format('YYYY-DD-MM')
    return this.state.schedule[dateString] === true || moment(day).startOf('day').diff(moment().startOf('day')) &lt; 0
  }</code>
<code class="language-markup">  renderConfirmationString() {
    const spanStyle = {color: '#00bcd4'}
    return this.state.confirmationTextVisible ? &lt;h2 style={{ textAlign: this.state.smallScreen ? 'center' : 'left', color: '#bdbdbd', lineHeight: 1.5, padding: '0 10px', fontFamily: 'Roboto'}}&gt;
      { &lt;span&gt;
        Scheduling a</code>
<code class="language-markup">          &lt;span style={spanStyle}&gt; 1 hour &lt;/span&gt;</code>
<code class="language-markup">        appointment {this.state.appointmentDate &amp;&amp; &lt;span&gt;
          on &lt;span style={spanStyle}&gt;{moment(this.state.appointmentDate).format('dddd[,] MMMM Do')}&lt;/span&gt;
      &lt;/span&gt;} {Number.isInteger(this.state.appointmentSlot) &amp;&amp; &lt;span&gt;at &lt;span style={spanStyle}&gt;{moment().hour(9).minute(0).add(this.state.appointmentSlot, 'hours').format('h:mm a')}&lt;/span&gt;&lt;/span&gt;}
      &lt;/span&gt;}
    &lt;/h2&gt; : null
  }</code>
<code class="language-markup">  renderAppointmentTimes() {
    if (!this.state.loading) {
      const slots = [...Array(8).keys()]
      return slots.map(slot =&gt; {
        const appointmentDateString = moment(this.state.appointmentDate).format('YYYY-DD-MM')
        const t1 = moment().hour(9).minute(0).add(slot, 'hours')
        const t2 = moment().hour(9).minute(0).add(slot + 1, 'hours')
        const scheduleDisabled = this.state.schedule[appointmentDateString] ? this.state.schedule[moment(this.state.appointmentDate).format('YYYY-DD-MM')][slot] : false
        const meridiemDisabled = this.state.appointmentMeridiem ? t1.format('a') === 'am' : t1.format('a') === 'pm'
        return &lt;RadioButton
          label={t1.format('h:mm a') + ' - ' + t2.format('h:mm a')}
          key={slot}
          value={slot}
          style={{marginBottom: 15, display: meridiemDisabled ? 'none' : 'inherit'}}
          disabled={scheduleDisabled || meridiemDisabled}/&gt;
      })
    } else {
      return null
    }
  }</code>
<code class="language-markup">  renderAppointmentConfirmation() {
    const spanStyle = { color: '#00bcd4' }
    return &lt;section&gt;
      &lt;p&gt;Name: &lt;span style={spanStyle}&gt;{this.state.firstName} {this.state.lastName}&lt;/span&gt;&lt;/p&gt;
      &lt;p&gt;Number: &lt;span style={spanStyle}&gt;{this.state.phone}&lt;/span&gt;&lt;/p&gt;
      &lt;p&gt;Email: &lt;span style={spanStyle}&gt;{this.state.email}&lt;/span&gt;&lt;/p&gt;
      &lt;p&gt;Appointment: &lt;span style={spanStyle}&gt;{moment(this.state.appointmentDate).format('dddd[,] MMMM Do[,] YYYY')}&lt;/span&gt; at &lt;span style={spanStyle}&gt;{moment().hour(9).minute(0).add(this.state.appointmentSlot, 'hours').format('h:mm a')}&lt;/span&gt;&lt;/p&gt;
    &lt;/section&gt;
  }</code>
<code class="language-markup">  resize() {
    this.setState({ smallScreen: window.innerWidth &lt; 768 })
  }</code>
<code class="language-markup">  componentWillMount() {
    async.series({
      configs(callback) {
        axios.get(HOST + 'api/config').then(res =&gt;
          callback(null, res.data.data)
        )
      },
      appointments(callback) {
        axios.get(HOST + 'api/appointments').then(res =&gt; {
          callback(null, res.data.data)
        })
      }
    }, (err,response) =&gt; {
      err ? this.handleFetchError(err) : this.handleFetch(response)
    })
    addEventListener('resize', this.resize)
  }</code>
<code class="language-markup">  componentWillUnmount() {
    removeEventListener('resize', this.resize)
  }</code>
<code class="language-markup">  render() {
    const { stepIndex, loading, navOpen, smallScreen, confirmationModalOpen, confirmationSnackbarOpen, ...data } = this.state
    const contactFormFilled = data.firstName &amp;&amp; data.lastName &amp;&amp; data.phone &amp;&amp; data.email &amp;&amp; data.validPhone &amp;&amp; data.validEmail
    const modalActions = [
      &lt;FlatButton
        label="Cancel"
        primary={false}
        onClick={() =&gt; this.setState({ confirmationModalOpen : false})} /&gt;,
      &lt;FlatButton
        label="Confirm"
        primary={true}
        onClick={() =&gt; this.handleSubmit()} /&gt;
    ]
    return (
      &lt;div&gt;
        &lt;AppBar
          title={data.siteTitle}
          onLeftIconButtonTouchTap={() =&gt; this.handleNavToggle() }/&gt;
        &lt;Drawer
          docked={false}
          width={300}
          open={navOpen}
          onRequestChange={(navOpen) =&gt; this.setState({navOpen})} &gt;
          &lt;img src={logo}
               style={{
                 height: 70,
                 marginTop: 50,
                 marginBottom: 30,
                 marginLeft: '50%',
                 transform: 'translateX(-50%)'
               }}/&gt;
          &lt;a style={{textDecoration: 'none'}} href={this.state.homePageUrl}&gt;&lt;MenuItem&gt;Home&lt;/MenuItem&gt;&lt;/a&gt;
          &lt;a style={{textDecoration: 'none'}} href={this.state.aboutPageUrl}&gt;&lt;MenuItem&gt;About&lt;/MenuItem&gt;&lt;/a&gt;
          &lt;a style={{textDecoration: 'none'}} href={this.state.contactPageUrl}&gt;&lt;MenuItem&gt;Contact&lt;/MenuItem&gt;&lt;/a&gt;</code>
<code class="language-markup">          &lt;MenuItem disabled={true}
                    style={{
                      marginLeft: '50%',
                      transform: 'translate(-50%)'
                    }}&gt;
            {"© Copyright " + moment().format('YYYY')}&lt;/MenuItem&gt;
        &lt;/Drawer&gt;
        &lt;section style={{
            maxWidth: !smallScreen ? '80%' : '100%',
            margin: 'auto',
            marginTop: !smallScreen ? 20 : 0,
          }}&gt;
          {this.renderConfirmationString()}
          &lt;Card style={{
              padding: '10px 10px 25px 10px',
              height: smallScreen ? '100vh' : null
            }}&gt;
            &lt;Stepper
              activeStep={stepIndex}
              linear={false}
              orientation="vertical"&gt;
              &lt;Step disabled={loading}&gt;
                &lt;StepButton onClick={() =&gt; this.setState({ stepIndex: 0 })}&gt;
                  Choose an available day for your appointment
                &lt;/StepButton&gt;
                &lt;StepContent&gt;
                  &lt;DatePicker
                      style={{
                        marginTop: 10,
                        marginLeft: 10
                      }}
                      value={data.appointmentDate}
                      hintText="Select a date"
                      mode={smallScreen ? 'portrait' : 'landscape'}
                      onChange={(n, date) =&gt; this.handleSetAppointmentDate(date)}
                      shouldDisableDate={day =&gt; this.checkDisableDate(day)}
                       /&gt;
                  &lt;/StepContent&gt;
              &lt;/Step&gt;
              &lt;Step disabled={ !data.appointmentDate }&gt;
                &lt;StepButton onClick={() =&gt; this.setState({ stepIndex: 1 })}&gt;
                  Choose an available time for your appointment
                &lt;/StepButton&gt;
                &lt;StepContent&gt;
                  &lt;SelectField
                    floatingLabelText="AM or PM"
                    value={data.appointmentMeridiem}
                    onChange={(evt, key, payload) =&gt; this.handleSetAppointmentMeridiem(payload)}
                    selectionRenderer={value =&gt; value ? 'PM' : 'AM'}&gt;
                    &lt;MenuItem value={0}&gt;AM&lt;/MenuItem&gt;
                    &lt;MenuItem value={1}&gt;PM&lt;/MenuItem&gt;
                  &lt;/SelectField&gt;
                  &lt;RadioButtonGroup
                    style={{ marginTop: 15,
                             marginLeft: 15
                           }}
                    name="appointmentTimes"
                    defaultSelected={data.appointmentSlot}
                    onChange={(evt, val) =&gt; this.handleSetAppointmentSlot(val)}&gt;
                    {this.renderAppointmentTimes()}
                  &lt;/RadioButtonGroup&gt;
                &lt;/StepContent&gt;
              &lt;/Step&gt;
              &lt;Step disabled={ !Number.isInteger(this.state.appointmentSlot) }&gt;
                &lt;StepButton onClick={() =&gt; this.setState({ stepIndex: 2 })}&gt;
                  Share your contact information with us and we'll send you a reminder
                &lt;/StepButton&gt;
                &lt;StepContent&gt;
                  &lt;section&gt;
                    &lt;TextField
                      style={{ display: 'block' }}
                      name="first_name"
                      hintText="First Name"
                      floatingLabelText="First Name"
                      onChange={(evt, newValue) =&gt; this.setState({ firstName: newValue })}/&gt;
                    &lt;TextField
                      style={{ display: 'block' }}
                      name="last_name"
                      hintText="Last Name"
                      floatingLabelText="Last Name"
                      onChange={(evt, newValue) =&gt; this.setState({ lastName: newValue })}/&gt;
                    &lt;TextField
                      style={{ display: 'block' }}
                      name="email"
                      hintText="name@mail.com"
                      floatingLabelText="Email"
                      errorText={data.validEmail ? null : 'Enter a valid email address'}
                      onChange={(evt, newValue) =&gt; this.validateEmail(newValue)}/&gt;
                    &lt;TextField
                      style={{ display: 'block' }}
                      name="phone"
                      hintText="(888) 888-8888"
                      floatingLabelText="Phone"
                      errorText={data.validPhone ? null: 'Enter a valid phone number'}
                      onChange={(evt, newValue) =&gt; this.validatePhone(newValue)} /&gt;
                    &lt;RaisedButton
                      style={{ display: 'block' }}
                      label={contactFormFilled ? 'Schedule' : 'Fill out your information to schedule'}
                      labelPosition="before"
                      primary={true}
                      fullWidth={true}
                      onClick={() =&gt; this.setState({ confirmationModalOpen: !this.state.confirmationModalOpen })}
                      disabled={!contactFormFilled || data.processed }
                      style={{ marginTop: 20, maxWidth: 100}} /&gt;
                  &lt;/section&gt;
                &lt;/StepContent&gt;
              &lt;/Step&gt;
            &lt;/Stepper&gt;
          &lt;/Card&gt;
          &lt;Dialog
            modal={true}
            open={confirmationModalOpen}
            actions={modalActions}
            title="Confirm your appointment"&gt;
            {this.renderAppointmentConfirmation()}
          &lt;/Dialog&gt;
          &lt;SnackBar
            open={confirmationSnackbarOpen || loading}
            message={loading ? 'Loading... ' : data.confirmationSnackbarMessage || ''}
            autoHideDuration={10000}
            onRequestClose={() =&gt; this.setState({ confirmationSnackbarOpen: false })} /&gt;
        &lt;/section&gt;
      &lt;/div&gt;
    )
  }
}</code>

Part 2: Building the Backend

1. Installations and Directory Structure

Our backend will be simple. All it needs to do is act as an intermediary between our frontend and Cosmic and handle interfacing with Twilio.
First, we’ll get our directory structure in place.
AppointmentScheduler
|
|--public
|--app.js
|--.gitignore
|--package.json
public
will be where we serve our built frontend from and 
.gitignore
will hide
node_modules
.
# .gitignore
node_modules
We’ll use the following packages:
  • axios
    for pushing Appointment objects to Cosmic
  • body-parser
    ,
    cors
    ,
    http
    ,
    morgan
    ,
    path
    , and
    express
    for server-ware
  • cosmicjs
    - the official client, for fetching objects from Cosmic
  • twilio
    - the official client, for sending confirmation texts
  • moment
    for parsing times
Run
yarn init
, then edit
package.json
with a start script so we can deploy on Cosmic.
{
    // etc...
  "scripts": {
        "start": "node app.js"
    }
}
Then, before we start working:
yarn add axios body-parser cors cosmicjs express express-session http moment morgan path twilio

2. Outline the Backend’s Structure

Our Express app will be fairly basic as far as configuration and middleware goes. For
Appointment
submissions we'll handle post requests at
/api/appointments
. We'll serve our site configs and appointments from
/api/config
and
/api/appointments
respectively. Finally, since our Frontend is an SPA, we'll serve
index.html
from
/
and redirect all other requests there.
Before getting into much logic, our server will start off looking like this:
const express = require('express')
const path = require('path')
const morgan = require('morgan')
const bodyParser = require('body-parser')
const cors = require('cors')
const config = require('./config')
const http = require('http')
const Cosmic = require('cosmicjs')
const twilio = require('twilio')
const moment = require('moment')
const axios = require('axios')
const config = {
  bucket: {
    slug: process.env.COSMIC_BUCKET,
    read_key: process.env.COSMIC_READ_KEY,
    write_key: process.env.COSMIC_WRITE_KEY
  },
  twilio: {
    auth: process.env.TWILIO_AUTH,
    sid: process.env.TWILIO_SID,
    number: process.env.TWILIO_NUMBER
  }
}
const app = express()
const env = process.env.NODE_ENV || 'development'
const twilioSid = config.twilio.sid
const twilioAuth = config.twilio.auth
const twilioClient = twilio(twilioSid, twilioAuth)
const twilioNumber = config.twilio.number
app.set('trust proxy', 1)
app.use(session({
  secret: 'sjcimsoc',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false }
}))
app.use(cors())
app.use(morgan('dev'))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(express.static(path.join(__dirname, 'public')))
app.set('port', process.env.PORT || 3000)
app.post('/api/appointments', (req, res) => {
    //handle posting new appointments to Cosmic
  //and sending a confirmation text with Twilio
})
app.get('/api/config', (req, res) => {
    //fetch configs from Cosmic, expose to frontend
})
app.get('/api/appointments', (req, res) => {
    //fetch appointments from Cosmic, expose to frontend without personal data
})
app.get('/', (req, res) => {
    res.send('index.html')
})
app.get('*', (req, res) => {
    res.redirect('/')
})
http.createServer(app).listen(app.get('port'), () =>
  console.log('Server running at: ' + app.get('port'))
)
Note: we’ll provide all
process.env
variables at deployment with Cosmic. Cosmic-specific variables are supplied automatically.

3. Handle Post Requests

Two things need to happen here. We’ll use the official Twilio client to send a text to the user, and we’ll use
axios
to make a POST request to the Cosmic JS API. Before doing both, we'll strip the user-inputted phone number of any non-digits and compute the time from the selected slot.
We have:
app.post('/api/appointments', (req, res) => {
  const appointment = req.body
  appointment.phone = appointment.phone.replace(/\D/g,'')
  const date = moment(appointment.date, 'YYYY-DD-MM').startOf('day')
  const time = date.hour(9).add(appointment.slot, 'hours')
  const smsBody = `${appointment.name}, this message is to confirm your appointment at ${time.format('h:mm a')} on ${date.format('dddd MMMM Do[,] YYYY')}.`
  //send confirmation message to user
  twilioClient.messages.create({
    to: '+1' + appointment.phone,
    from: twilioNumber,
    body: smsBody
  }, (err, message) => console.log(message, err))
  //push to cosmic
  const cosmicObject = {
    "title": appointment.name,
    "type_slug": "appointments",
    "write_key": config.bucket.write_key,
    "metafields": [
      {
        "key": "date",
        "type": "text",
        "value": date.format('YYYY-DD-MM')
      },
      {
        "key": "slot",
        "type": "text",
        "value": appointment.slot
      },
      {
        "key": "email",
        "type": "text",
        "value": appointment.email
      },{
        "key": "phone",
        "type": "text",
        "value": appointment.phone //which is now stripped of all non-digits
      }
    ]
  }
  axios.post(`https://api.cosmicjs.com/v1/${config.bucket.slug}/add-object`, cosmicObject)
  .then(response => res.json({ data: 'success' })).catch(err => res.json({ data: 'error '}))
})

4. Expose Site Configs

We’ll simply use
cosmicjs
to get the
site-config
object we need for the frontend to display links in the navigation.
app.get('/api/config', (req,res) => {
  Cosmic.getObject(config, { slug: 'site-config' }, (err, response) => {
    const data = response.object.metadata
    err ? res.status(500).json({ data: 'error' }) : res.json({ data })
  })
})

5. Expose Appointments

Where it’s definitely redundant to expose the site configurations through our backends API, its definitely important that we’re doing it with the
Appointment
objects. First, we can conveniently reorganize the data to expose only what we need, but second, and infinitely more important, we don't publicly expose our users' personal information. We'll use
cosmicjs
to fetch all
Appointment
objects, but only expose an array of objects with the form
{ date, slot }
.
app.get('/api/appointments', (req, res) => {
  Cosmic.getObjectType(config, { type_slug: 'appointments' }, (err, response) => {
    const appointments = response.objects.all ? response.objects.all.map(appointment => {
      return {
        date: appointment.metadata.date,
        slot: appointment.metadata.slot
      }
    }) : {}
    res.json({ data: appointments })
  })
})

6. The Finished Product

Within minutes, entirely thanks to the simplicity of Express, CosmicJs’s official client, and Twilio’s official client, we have a backend that does everything we wanted it to do and nothing more. Pure zen.
const express = require('express')
const path = require('path')
const morgan = require('morgan')
const bodyParser = require('body-parser')
const cors = require('cors')
const config = require('./config')
const http = require('http')
const Cosmic = require('cosmicjs')
const twilio = require('twilio')
const moment = require('moment')
const axios = require('axios')
const config = {
  bucket: {
    slug: process.env.COSMIC_BUCKET,
    read_key: process.env.COSMIC_READ_KEY,
    write_key: process.env.COSMIC_WRITE_KEY
  },
  twilio: {
    auth: process.env.TWILIO_AUTH,
    sid: process.env.TWILIO_SID,
    number: process.env.TWILIO_NUMBER
  }
}
const app = express()
const env = process.env.NODE_ENV || 'development'
const twilioSid = config.twilio.sid
const twilioAuth = config.twilio.auth
const twilioClient = twilio(twilioSid, twilioAuth)
const twilioNumber = config.twilio.number
app.set('trust proxy', 1)
app.use(session({
  secret: 'sjcimsoc',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false }
}))
app.use(cors())
app.use(morgan('dev'))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(express.static(path.join(__dirname, 'public')))
app.set('port', process.env.PORT || 3000)
app.post('/api/appointments', (req, res) => {
  const appointment = req.body
  appointment.phone = appointment.phone.replace(/\D/g,'')
  const date = moment(appointment.date, 'YYYY-DD-MM').startOf('day')
  const time = date.hour(9).add(appointment.slot, 'hours')
  const smsBody = `${appointment.name}, this message is to confirm your appointment at ${time.format('h:mm a')} on ${date.format('dddd MMMM Do[,] YYYY')}.`
  //send confirmation message to user
  twilioClient.messages.create({
    to: '+1' + appointment.phone,
    from: twilioNumber,
    body: smsBody
  }, (err, message) => console.log(message, err))
  //push to cosmic
  const cosmicObject = {
    "title": appointment.name,
    "type_slug": "appointments",
    "write_key": config.bucket.write_key,
    "metafields": [
      {
        "key": "date",
        "type": "text",
        "value": date.format('YYYY-DD-MM')
      },
      {
        "key": "slot",
        "type": "text",
        "value": appointment.slot
      },
      {
        "key": "email",
        "type": "text",
        "value": appointment.email
      },{
        "key": "phone",
        "type": "text",
        "value": appointment.phone //which is now stripped of all non-digits
      }
    ]
  }
  axios.post(`https://api.cosmicjs.com/v1/${config.bucket.slug}/add-object`, cosmicObject)
  .then(response => res.json({ data: 'success' })).catch(err => res.json({ data: 'error '}))
})
app.get('/api/config', (req,res) => {
  Cosmic.getObject(config, { slug: 'site-config' }, (err, response) => {
    const data = response.object.metadata
    err ? res.status(500).json({ data: 'error' }) : res.json({ data })
  })
})
app.get('/api/appointments', (req, res) => {
  Cosmic.getObjectType(config, { type_slug: 'appointments' }, (err, response) => {
    const appointments = response.objects.all ? response.objects.all.map(appointment => {
      return {
        date: appointment.metadata.date,
        slot: appointment.metadata.slot
      }
    }) : {}
    res.json({ data: appointments })
  })
})
app.get('/', (req, res) => {
    res.send('index.html')
})
app.get('*', (req, res) => {
    res.redirect('/')
})
http.createServer(app).listen(app.get('port'), () =>
  console.log('Server running at: ' + app.get('port'))
)

Part 3: Build and Deploy

Before we build an extension to manage our Appointments, we’ll bundle the frontend and deploy the app to Cosmic so we can have even have some appointments to manage.
In the frontend directory,
appointment-scheduler
, run
webpack
to build out into
dist
. Then move the contents of
dist
to the backend's public folder -
AppointmentScheduler/public
. The
index.html
that Webpack builds will then be the
index.html
we serve from 
/
.
From
AppointmentScheduler
, commit the app to a new Github repo. Then, create a trial Twilio account and within the Cosmic JS dashboard, add the following
env
variables from the deploy menu.
  • TWILIO_AUTH
    - your Twilio auth key
  • TWILIO_SID
    - your Twilio sid
  • TWILIO_NUMBER
    - the phone number you have associated with your Twilio trial.
Now go ahead and deploy and add a few sample appointments that we can use to test our extension.

Part 4. Build the Extension

Cosmic JS let’s you upload SPA’s that you can use to access and manipulate your bucket’s data right within the Cosmic JS dashboard. These are called Extensions and we’ll be building one to be able to view a table of all scheduled appointments, as well as providing us with an easy way to delete them.
Just as with the frontend, we’ll be using React with Material UI and the steps here will be similar to Part 1.

1. Boilerplate setup

First, make our
appointment-scheduler-extension
directory, run
yarn init
, and create the following project structure.
appointment-scheduler-extension
|
|--dist
|--src
|. |--Components
|. |. |--App.js
|. |--index.html
|. |--index.js
|--.babelrc
|--.gitignore
|--package.json
|--webpack.config.js
Use the same exact
index.html
template we used for the frontend.
<!-- ./src/index.html -->
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <link href="https://fonts.googleapis.com/css?family=Roboto" rel="stylesheet">
    <title>Appointment Scheduler</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
  1. We’ll be using almost all of the same packages as the frontend. Refer to Part 1 for installations, including adding
    lodash
    and
    query-string
    . We'll use
    lodash
    for filtering data and
    query-string
    as a convenient way to get our Cosmic keys, which Cosmic supplies as url parameters.
  2. Likewise,
    webpack.config.js
    .gitignore
    , and 
    .babelrc
    will all be entirely the same as in Part 1.
  3. index.js
    won't change either besides a new scheme for config variables:
  4. import React from ‘react’ 
    import ReactDom from ‘react-dom’ 
    import App from ‘./Components/App’ 
    import MuiThemeProvider from ‘material-ui/styles/MuiThemeProvider’ 
    import QueryString from ‘query-string’ 
    import ‘normalize.css’ 
    
    window.React = React 
    const url = QueryString.parse(location.search) 
    const config = { 
    	bucket: { 
    		slug: url.bucket_slug, 
    		write_key: url.write_key, 
    		read_key: url.read_key 
    	} 
    } 
    
    ReactDom.render( 
    	<MuiThemeProvider> <App config={config}/> </MuiThemeProvider>, 
    	document.getElementById(‘root’) 
    )

3. Work Out a Skeleton

From here, our extesion and frontend start to diverge.
At it’s barest, our extension will look like this:
import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
injectTapEventPlugin()
export default class App extends Component {
    constructor(props) {
        super(props)
      // set initial state
      // bind component methods
    }
  
  // component methods, lifecycle methods
  
  render() {
        return (
        //Material UI components 
        )
    }
}
Thinking about what we need for an initial state:
  • We’re getting our Cosmic config variables from url parametes in
    index.js 
    and we're passing them to
    App
    as props, so we'll need to move those to it's state.
  • We’ll be using a
    SnackBar
    like we did in the frontend so we need to keep track of its state and message. We'll also be using a
    DatePicker
    and need a similar strategy.
  • We’ll have a toolbar with a dropdown that lets the user select between listing all appointments for filtering them by day. We’ll track which is being done by assigning
    1
    to a state variable when the first is selected,
    0
    for the latter.
  • We’re loading our appointments data from Cosmic so it will be useful to cache them. We’ll also need to do this separately for filtering appointments by date.
  • We can select rows in the table of appointments and need to track which are selected. It will also be useful to track the state of all rows being selected.
Thus, for an initial state, we have:
this.state = {
  config: props.config,
  snackbarDisabled: false,
  snackbarMessage: 'Loading...',
  toolbarDropdownValue: 1,
  appointments: {},
  filteredAppointments: {},
  datePickerDisabled: true,
  selectedRows: [],
  deleteButtonDisabled: true,
  allRowsSelected: false
}

4. Draft Out Functionality

Our Extension needs to have the following functions to get it working the way we need it to:
  • Fetch data from Cosmic JS in
    componentWillMount()
    and handle it in a seperate
    handleFetchMethod
    (and it's companion
    handleFetchError()
    )
  • Change the state when the filter option is changed with
    handleToobarDropdownChange()
  • Override default Material UI
    Table
    selections with
    handleRowSelection()
  • Handle deleting
    Appointment
    objects with
    handleDelete()
  • Feed disabled dates to the
    DatePicker
    with
    checkDisableDate()
  • Filter appointments with
    filterAppointments()
  • Render appointments as
    TableRow
    s with
    setTableChildren()
Including those in our extension’s skeleton, we now have:
import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
injectTapEventPlugin()
export default class App extends Component {
    constructor(props) {
        super(props)
      this.state = {
          config: props.config,
          snackbarDisabled: false,
          snackbarMessage: 'Loading...',
          toolbarDropdownValue: 1,
          appointments: {},
          filteredAppointments: {},
          datePickerDisabled: true,
          selectedRows: [],
          deleteButtonDisabled: true,
          allRowsSelected: false
        }
    }
  
  handleFetchError(err) {
        //handle errors fetching data from Cosmic JS
    }
  
  handleFetch(response) {
        //process data fetched from Cosmic JS
    }
  
  handleToolbarDropdownChange(val) {
        // set the dropdown value and clear filteredAppointments() if
      // "List All" is selected. (State 1). 
    }
  
  handleRowSelection(rowsToSelect) {
        // Table returns 'all' if the select-all button was used, an array of selected
      // row numbers, otherwise. We need to make sense of this.
    }
  
  handleDelete(selectedRows) {
        //send a post request to Cosmic JS's api to get rid of unwanted appointments
    }
  
  checkDisableDate(date) {
        //feed the DatePicker days based on availability determined by appointments
      //retrieved from Cosmic
    }
  
  filterAppointments(date) {
        //Only show appointments occuring on date
    }
  
 setTableChildren(selectedRows = this.state.selectedRows, appointments = this.state.appointments) {
        //render a TableRow for each appointment loaded
    }
  
  componentWillMount() {
        //fetch data immediately
    }
  
  render() {
        return (
        //Material UI components 
        )
    }
}

5. Build Out the View

Like the frontend, we’ll use all Material UI components to display our data.
render() {
  const { snackbarDisabled, appointments, datePickerDisabled, deleteButtonDisabled, ...data } = this.state
  return (
    <div style={{ fontFamily: 'Roboto' }}>
      <AppBar
        showMenuIconButton={false}
        title="Appointment Manager"/>
      <SnackBar
        message={data.snackbarMessage}
        open={!snackbarDisabled} />
      <Toolbar>
        <ToolbarGroup firstChild={true}>
          <DropDownMenu
            value={data.toolbarDropdownValue}
            onChange={(evt, key, val) => this.handleToolbarDropdownChange(val)}>
            <MenuItem value={0} primaryText="Filter Appointments By Date" />
            <MenuItem value={1} primaryText="List All Appointments" />
          </DropDownMenu>
          <DatePicker
            hintText="Select a date"
            autoOk={true}
            disabled={datePickerDisabled}
            name="date-select"
            onChange={(n, date) => this.filterAppointments(date)}
            shouldDisableDate={(day) => this.checkDisableDate(day)} />
        </ToolbarGroup>
        <ToolbarGroup lastChild={true}>
          <RaisedButton
            primary={true}
            onClick={() => this.handleDelete(data.selectedRows)}
            disabled={deleteButtonDisabled}
            label={`Delete Selected ${data.selectedRows.length ? '(' + data.selectedRows.length + ')' : ''}`} />
        </ToolbarGroup>
      </Toolbar>
      <Table
        onRowSelection={rowsToSelect => this.handleRowSelection(rowsToSelect)}
        multiSelectable={true} >
        <TableHeader>
          <TableRow>
            <TableHeaderColumn>ID</TableHeaderColumn>
            <TableHeaderColumn>Name</TableHeaderColumn>
            <TableHeaderColumn>Email</TableHeaderColumn>
            <TableHeaderColumn>Phone</TableHeaderColumn>
            <TableHeaderColumn>Date</TableHeaderColumn>
            <TableHeaderColumn>Time</TableHeaderColumn>
          </TableRow>
        </TableHeader>
        <TableBody
          children={data.tableChildren}
          allRowsSelected={data.allRowsSelected}>
        </TableBody>
      </Table>
    </div>
  )
}

6. Fetch Appointment Data

Unllike with the frontend, we don’t have to worry about exposing sensitive data to the public, so we can easily use
cosmicjs
to handle the fetch. We'll do this in
componentWillMount()
.
componentWillMount() {
  Cosmic.getObjectType(this.state.config, { type_slug: 'appointments' }, (err, response) => err ? this.handleFetchError(err) : this.handleFetch(response)
  )
}
We’ll handle errors with
handleFetchError()
, which will show the user that an error occured in the
SnackBar
.
handleFetchError(err) {
  console.log(err)
  this.setState({ snackbarMessage: 'Error loading data' })
}
If data is successfully returned, we’ll process it with
handleFetch()
.
handleFetch(response) {
  const appointments = response.objects.all ? response.objects.all.reduce((currentAppointments, appointment) => {
    const date = appointment.metadata.date
    if (!currentAppointments[date]) currentAppointments[date] = []
    const appointmentData = {
      slot: appointment.metadata.slot,
      name: appointment.title,
      email: appointment.metadata.email,
      phone: appointment.metadata.phone,
      slug: appointment.slug
    }
    currentAppointments[date].push(appointmentData)
    currentAppointments[date].sort((a,b) => a.slot - b.slot)
    return currentAppointments
  }, {}) : {}
  this.setState({ appointments, snackbarDisabled: true, tableChildren: this.setTableChildren([], appointments) })
}
From the array of
Appointment
objects our bucket sends, we create a schedule of all loaded appointments, appointments. We then save that to the state and pass it to
setTableChildren()
to use in rendering the 
Table
.

7. Handle UI Changes

We need a few simple methods to handle the filter dropdown in the toolbar, selecting rows, filtering appointments, feeding a check for disabling dates to the
DatePicker
. Starting with handling the dropdown filter,
0
maps to filtering the appointments by date,
1
maps to listing all. For listing all, we reset
state.filteredAppointments
.
handleToolbarDropdownChange(val) {
  //0: filter by date, 1: list all
  val ? this.setState({ filteredAppointments: {}, datePickerDisabled: true, toolbarDropdownValue: 1 }) : this.setState({ toolbarDropdownValue: 0, datePickerDisabled: false })
}
For handling row selection, we save the selected rows to the state, set the table children based on the rows selected, and enable the delete button if at least one row is selected.
handleRowSelection(rowsToSelect) {
  const allRows = [...Array(this.state.tableChildren.length).keys()]
  const allRowsSelected = rowsToSelect === 'all'
  const selectedRows = Array.isArray(rowsToSelect) ? rowsToSelect : allRowsSelected ? allRows : []
  const appointments = _.isEmpty(this.state.filteredAppointments) ? this.state.appointments : this.state.filteredAppointments
  const deleteButtonDisabled = selectedRows.length == 0
  const tableChildren = allRowsSelected ? this.setTableChildren([], appointments) : this.setTableChildren(selectedRows, appointments)
  this.setState({ selectedRows, deleteButtonDisabled, tableChildren })
}
For disabling dates, we only make them active if
state.appointments.date
, where
date = 'YYYY-DD-MM'
, exists.
checkDisableDate(day) {
  return !this.state.appointments[moment(day).format('YYYY-DD-MM')]
}

8. Filtering Appointments and Rendering the Table

When the user changes the filter dropdown to
Filter By Date
they then pick a date from the date picker. Upon choosing a date, the date picker fires
filterAppointments()
to set
state.filteredAppoitments
to the sub-schedule
state.appointments[selectedDate]
and pass that sub-schedule
setTableChildren()
.
filterAppointments(date) {
  const dateString = moment(date).format('YYYY-DD-MM')
  const filteredAppointments = {}
  filteredAppointments[dateString] = this.state.appointments[dateString]
  this.setState({ filteredAppointments, tableChildren: this.setTableChildren([], filteredAppointments) })
}
When
filterAppointments()
(or any other method) calls
setTableChildren()
we can optionally pass an array of selected rows and an
appointments
object or let it default to
state.selectedRows
and
state.appointments
. If the appointments are filtered, we sort them by time before rendering.
setTableChildren(selectedRows = this.state.selectedRows, appointments = this.state.appointments) {
  const renderAppointment = (date, appointment, index) => {
    const { name, email, phone, slot } = appointment
    const rowSelected = selectedRows.includes(index)
    return <TableRow key={index} selected={rowSelected}>
      <TableRowColumn>{index}</TableRowColumn>
      <TableRowColumn>{name}</TableRowColumn>
      <TableRowColumn>{email}</TableRowColumn>
      <TableRowColumn>{phone}</TableRowColumn>
      <TableRowColumn>{moment(date, 'YYYY-DD-MM').format('M[/]D[/]YYYY')}</TableRowColumn>
      <TableRowColumn>{moment().hour(9).minute(0).add(slot, 'hours').format('h:mm a')}</TableRowColumn>
    </TableRow>
  }
  const appointmentsAreFiltered = !_.isEmpty(this.state.filteredAppointments)
  const schedule = appointmentsAreFiltered ? this.state.filteredAppointments : appointments
  const els = []
  let counter = 0
  appointmentsAreFiltered ?
    Object.keys(schedule).forEach(date => {
    schedule[date].forEach((appointment, index) => els.push(renderAppointment(date, appointment, index)))
  }) :
  Object.keys(schedule).sort((a,b) => moment(a, 'YYYY-DD-MM').isBefore(moment(b, 'YYYY-MM-DD')))
    .forEach((date, index) => {
    schedule[date].forEach(appointment => {
      els.push(renderAppointment(date, appointment, counter))
      counter++
    })
  })
  return els
}

9. Deleting Appointments

The last thing we need to handle is letting the user delete appointments, leveraging
cosmicjs
for deleting objects found using
lodash
from
state.appointments
according to
selectedRows
.
handleDelete(selectedRows) {
  const { config } = this.state
  return selectedRows.map(row => {
    const { tableChildren, appointments } = this.state
    const date = moment(tableChildren[row].props.children[4].props.children, 'M-D-YYYY').format('YYYY-DD-MM')
    const slot = moment(tableChildren[row].props.children[5].props.children, 'h:mm a').diff(moment().hours(9).minutes(0).seconds(0), 'hours') + 1
    return _.find(appointments[date], appointment =>
                  appointment.slot === slot
                 )
  }).map(appointment => appointment.slug).forEach(slug =>
                                                  Cosmic.deleteObject(config, { slug, write_key: config.bucket.write_key }, (err, response) => {
    if (err) {
      console.log(err)
      this.setState({ snackbarDisabled: false, snackbarMessage: 'Failed to delete appointments' })
    } else {
      this.setState({ snackbarMessage: 'Loading...', snackbarDisabled: false })
      Cosmic.getObjectType(this.state.config, { type_slug: 'appointments' }, (err, response) =>
                           err ? this.handleFetchError(err) : this.handleFetch(response)
                          )}
  }
                                                                     )
                                                 )
  this.setState({ selectedRows: [], deleteButtonDisabled: true})
}

10. Putting It All Together

At this point, including all necessary imports, our completed extension looks like this:
// ./src/Components/App.js
import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
import axios from 'axios'
import async from 'async'
import _ from 'lodash'
import moment from 'moment'
import Cosmic from 'cosmicjs'
import AppBar from 'material-ui/AppBar'
import FlatButton from 'material-ui/FlatButton'
import RaisedButton from 'material-ui/RaisedButton'
import SnackBar from 'material-ui/SnackBar'
import DropDownMenu from 'material-ui/DropDownMenu'
import MenuItem from 'material-ui/MenuItem'
import DatePicker from 'material-ui/DatePicker'
import {
  Toolbar,
  ToolbarGroup
} from 'material-ui/Toolbar'
import {
  Table,
  TableBody,
  TableHeader,
  TableHeaderColumn,
  TableRow,
  TableRowColumn,
} from 'material-ui/Table';
injectTapEventPlugin()
export default class App extends Component {
  constructor(props) {
    super(props)
    this.state = {
      config: props.config,
      snackbarDisabled: false,
      snackbarMessage: 'Loading...',
      toolbarDropdownValue: 1,
      appointments: {},
      filteredAppointments: {},
      datePickerDisabled: true,
      selectedRows: [],
      deleteButtonDisabled: true,
      allRowsSelected: false
    }
    this.handleFetchError = this.handleFetchError.bind(this)
    this.handleFetch = this.handleFetch.bind(this)
    this.handleRowSelection = this.handleRowSelection.bind(this)
    this.handleToolbarDropdownChange = this.handleToolbarDropdownChange.bind(this)
    this.handleDelete = this.handleDelete.bind(this)
    this.checkDisableDate = this.checkDisableDate.bind(this)
    this.setTableChildren = this.setTableChildren.bind(this)
  }
  handleFetchError(err) {
    console.log(err)
    this.setState({ snackbarMessage: 'Error loading data' })
  }
  handleFetch(response) {
    const appointments = response.objects.all ? response.objects.all.reduce((currentAppointments, appointment) => {
      const date = appointment.metadata.date
      if (!currentAppointments[date]) currentAppointments[date] = []
      const appointmentData = {
        slot: appointment.metadata.slot,
        name: appointment.title,
        email: appointment.metadata.email,
        phone: appointment.metadata.phone,
        slug: appointment.slug
      }
      currentAppointments[date].push(appointmentData)
      currentAppointments[date].sort((a,b) => a.slot - b.slot)
      return currentAppointments
    }, {}) : {}
    this.setState({ appointments, snackbarDisabled: true, tableChildren: this.setTableChildren([], appointments) })
  }
  handleToolbarDropdownChange(val) {
    //0: filter by date, 1: list all
    val ? this.setState({ filteredAppointments: {}, datePickerDisabled: true, toolbarDropdownValue: 1 }) : this.setState({ toolbarDropdownValue: 0, datePickerDisabled: false })
  }
  handleRowSelection(rowsToSelect) {
    const allRows = [...Array(this.state.tableChildren.length).keys()]
    const allRowsSelected = rowsToSelect === 'all'
    const selectedRows = Array.isArray(rowsToSelect) ? rowsToSelect : allRowsSelected ? allRows : []
    const appointments = _.isEmpty(this.state.filteredAppointments) ? this.state.appointments : this.state.filteredAppointments
    const deleteButtonDisabled = selectedRows.length == 0
    const tableChildren = allRowsSelected ? this.setTableChildren([], appointments) : this.setTableChildren(selectedRows, appointments)
    this.setState({ selectedRows, deleteButtonDisabled, tableChildren })
  }
  handleDelete(selectedRows) {
    const { config } = this.state
    return selectedRows.map(row => {
      const { tableChildren, appointments } = this.state
      const date = moment(tableChildren[row].props.children[4].props.children, 'M-D-YYYY').format('YYYY-DD-MM')
      const slot = moment(tableChildren[row].props.children[5].props.children, 'h:mm a').diff(moment().hours(9).minutes(0).seconds(0), 'hours') + 1
      return _.find(appointments[date], appointment =>
        appointment.slot === slot
      )
    }).map(appointment => appointment.slug).forEach(slug =>
      Cosmic.deleteObject(config, { slug, write_key: config.bucket.write_key }, (err, response) => {
        if (err) {
          console.log(err)
          this.setState({ snackbarDisabled: false, snackbarMessage: 'Failed to delete appointments' })
        } else {
          this.setState({ snackbarMessage: 'Loading...', snackbarDisabled: false })
          Cosmic.getObjectType(this.state.config, { type_slug: 'appointments' }, (err, response) =>
            err ? this.handleFetchError(err) : this.handleFetch(response)
          )}
        }
      )
    )
    this.setState({ selectedRows: [], deleteButtonDisabled: true})
  }
  checkDisableDate(day) {
    return !this.state.appointments[moment(day).format('YYYY-DD-MM')]
  }
  filterAppointments(date) {
    const dateString = moment(date).format('YYYY-DD-MM')
    const filteredAppointments = {}
    filteredAppointments[dateString] = this.state.appointments[dateString]
    this.setState({ filteredAppointments, tableChildren: this.setTableChildren([], filteredAppointments) })
  }
  setTableChildren(selectedRows = this.state.selectedRows, appointments = this.state.appointments) {
    const renderAppointment = (date, appointment, index) => {
      const { name, email, phone, slot } = appointment
      const rowSelected = selectedRows.includes(index)
      return <TableRow key={index} selected={rowSelected}>
        <TableRowColumn>{index}</TableRowColumn>
        <TableRowColumn>{name}</TableRowColumn>
        <TableRowColumn>{email}</TableRowColumn>
        <TableRowColumn>{phone}</TableRowColumn>
        <TableRowColumn>{moment(date, 'YYYY-DD-MM').format('M[/]D[/]YYYY')}</TableRowColumn>
        <TableRowColumn>{moment().hour(9).minute(0).add(slot, 'hours').format('h:mm a')}</TableRowColumn>
      </TableRow>
    }
    const appointmentsAreFiltered = !_.isEmpty(this.state.filteredAppointments)
    const schedule = appointmentsAreFiltered ? this.state.filteredAppointments : appointments
    const els = []
    let counter = 0
    appointmentsAreFiltered ?
      Object.keys(schedule).forEach(date => {
        schedule[date].forEach((appointment, index) => els.push(renderAppointment(date, appointment, index)))
      }) :
      Object.keys(schedule).sort((a,b) => moment(a, 'YYYY-DD-MM').isBefore(moment(b, 'YYYY-MM-DD')))
      .forEach((date, index) => {
        schedule[date].forEach(appointment => {
          els.push(renderAppointment(date, appointment, counter))
          counter++
        })
      })
    return els
  }
  componentWillMount() {
    Cosmic.getObjectType(this.state.config, { type_slug: 'appointments' }, (err, response) =>
      err ? this.handleFetchError(err) : this.handleFetch(response)
    )
  }
  render() {
    const { snackbarDisabled, appointments, datePickerDisabled, deleteButtonDisabled, ...data } = this.state
    return (
      <div style={{ fontFamily: 'Roboto' }}>
        <AppBar
          showMenuIconButton={false}
          title="Appointment Manager"/>
        <SnackBar
          message={data.snackbarMessage}
          open={!snackbarDisabled} />
        <Toolbar>
          <ToolbarGroup firstChild={true}>
            <DropDownMenu
              value={data.toolbarDropdownValue}
              onChange={(evt, key, val) => this.handleToolbarDropdownChange(val)}>
              <MenuItem value={0} primaryText="Filter Appointments By Date" />
              <MenuItem value={1} primaryText="List All Appointments" />
            </DropDownMenu>
            <DatePicker
              hintText="Select a date"
              autoOk={true}
              disabled={datePickerDisabled}
              name="date-select"
              onChange={(n, date) => this.filterAppointments(date)}
              shouldDisableDate={(day) => this.checkDisableDate(day)} />
          </ToolbarGroup>
          <ToolbarGroup lastChild={true}>
            <RaisedButton
              primary={true}
              onClick={() => this.handleDelete(data.selectedRows)}
              disabled={deleteButtonDisabled}
              label={`Delete Selected ${data.selectedRows.length ? '(' + data.selectedRows.length + ')' : ''}`} />
          </ToolbarGroup>
        </Toolbar>
        <Table
          onRowSelection={rowsToSelect => this.handleRowSelection(rowsToSelect)}
          multiSelectable={true} >
          <TableHeader>
            <TableRow>
              <TableHeaderColumn>ID</TableHeaderColumn>
              <TableHeaderColumn>Name</TableHeaderColumn>
              <TableHeaderColumn>Email</TableHeaderColumn>
              <TableHeaderColumn>Phone</TableHeaderColumn>
              <TableHeaderColumn>Date</TableHeaderColumn>
              <TableHeaderColumn>Time</TableHeaderColumn>
            </TableRow>
          </TableHeader>
          <TableBody
            children={data.tableChildren}
            allRowsSelected={data.allRowsSelected}>
          </TableBody>
        </Table>
      </div>
    )
  }
}

Part 5: Conclusion

Once you run
webpack
in
appointment-scheduler-extension
, create
extension.json
in
dist
to make Cosmic recognize it:
// appointment-scheduler-extension/dist/extension.json
{
  "title": "Appointment Manager",
  "font_awesome_class": "fa-calendar",
  "image_url": ""
}
Then, compress
dist
, upload it to Cosmic, and we're ready to start managing appointments.
Using Cosmic JS, Twilio, Express, and React, we’ve built a modular, easy to extend appointment scheduler to both give others easy access to our time while saving more of it for ourselves. The speed at which we’re able to get our app deployed and the simplicity of managing our data reinforces that it was an obvious choice to use Cosmic JS both for CMS and deployment.
Although our appointment scheduler will definitely save us time in the future, it’s a sure thing that it can never compete with the time Cosmic will save us on future projects.
Matt Cain builds smart web applications and writes about the tech used to build them. You can learn more about him on his portfolio.

Written by tonyspiro | CEO & Co-Founder of Cosmic JS
Published by HackerNoon on 2020/03/13