TL;DR View the demo Install the App View the codebase 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 to build an appointment scheduler. That way, the people who need to talk to you only have to once. Cosmic JS 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 object type with the specified metafields. Then, we'll create a object type with no default metafields and a Siteobject in which we'll define object-specific metafields. Appointments Configs Appointments Here, and will be the user's metadata — we'll use their name as the Appointment object's title. will hold the appointment date in format and will be how many hours away the appointment is from 9AM. email phone Date YYYY-DD-MM slot / Configs Site We’re using the object to define details about the app that we want to be able to change on the fly, rather than having to redeploy for. Config 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: Appointment Scheduler <!-- ./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 </ > 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: for an easy way to make ajax calls in series async as a useful ajax utility axios to make use of the const { property } = object destructuring pattern babel-preset-stage-3 for a convenient set of React-built Material Design components material-ui for parsing times moment to clear browser-default styles normalize.css - a necessary companion to react-tap-event-plugin material-ui To install everything we need, run yarn axios babel-preset-stage material-ui moment normalze.css react react-dom react-tap- -plugin add async -3 event and for our dev-dependencies, css-loader eslint file-loader html-webpack-plugin path style-loader webpack yarn add babel-core babel-loader babel-preset-env babel-preset-react 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 path = (‘path’) HtmlWebpackPlugin = (‘html-webpack-plugin’) webpack = (‘webpack’) .exports = { : ‘./src/index.js’, : { : path.resolve(‘dist’), : ‘bundle.js’, : ‘bundle.map.js’ }, : ‘source-map’, : { : }, : { : [{ : , : { : ‘babel-loader’ }, : path.resolve(‘node_modules’) }, { : [ ,/\.css$/], : [‘style-loader’, ‘css-loader’, ‘sass-loader’] }, { : , : [ { : ‘file-loader’ } ] }] }, : [ HtmlWebpackPlugin({ : ‘./src/index.html’, : ‘index.html’, : , : }), webpack.DefinePlugin({ : process.env.NODE_ENV === ‘production’ }) ] } const require const require const require module entry output path filename sourceMapFilename devtool devServer port 8080 module rules test /\.js$/ use loader exclude test /\.scss$/ loader test /\.(png|jpg|gif|svg)$/ use loader plugins new template filename inject true xhtml true new PRODUCTION path = (‘path’) HtmlWebpackPlugin = (‘html-webpack-plugin’) webpack = (‘webpack’) .exports = { : ‘./src/index.js’, : { : path.resolve(‘dist’), : ‘bundle.js’, : ‘bundle.map.js’ }, : ‘source-map’, : { : }, : { : [{ : , : { : ‘babel-loader’ }, : path.resolve(‘node_modules’) }, { : [ ,/\.css$/], : [‘style-loader’, ‘css-loader’, ‘sass-loader’] }, { : , : [ { : ‘file-loader’ } ] }] }, : [ HtmlWebpackPlugin({ : ‘./src/index.html’, : ‘index.html’, : , : }), webpack.DefinePlugin({ : process.env.NODE_ENV === ‘production’ }) ] } const require const require const require module entry output path filename sourceMapFilename devtool devServer port 8080 module rules test /\.js$/ use loader exclude test /\.scss$/ loader test /\.(png|jpg|gif|svg)$/ use loader plugins new template filename inject true xhtml true new PRODUCTION We’ve configured Webpack to output , it's source map, and (according to the template in ) to on building. You also have the option to use SCSS throughout the project and have access to your Node environment via at build-time. bundle.js index.html src dist window.PRODUCTION 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 file and it will look like this: src/index.js React ReactDom App MuiThemeProvider ( ) .React = React ReactDom.render( <App /> , .getElementById( ) ) // ./src/index.js import from 'react' import from 'react-dom' import from './Components/App' import from 'material-ui/styles/MuiThemeProvider' import 'normalize.css' require './scss/app.scss' window < > MuiThemeProvider </ > MuiThemeProvider document '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: { Component } injectTapEventPlugin injectTapEventPlugin() { () { () .state = { } } componentWillMount() { } componentWillUnmount() { } render() { ( ) } } // ./src/Components/App.js import from 'react' import from 'react-tap-event-plugin' export default class App extends Component constructor super this // initial state //method bindings //component methods? //lifecycle methods //fetch data from cosmic, watch window width //remove window width event listener //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: .state = { : , : , : , : , : , : , : , : , : , : .innerWidth < , : } // ./src/Components/App.js // ... this loading true navOpen false confirmationModalOpen false confirmationTextVisible false stepIndex 0 appointmentDateSelected false appointmentMeridiem 0 validEmail false validPhone false smallScreen window 768 confirmationSnackbarOpen false Note that takes on or , such that and . appointmentMeridiem 0 1 0 => 'AM' 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 method to display/hide it handleNavToggle() The three scheduling steps are revealed to the user in succession upon completing a previous step, so we need a method to handle the flow of user input handleNextStep() We’ll user a Material UI date picker to set our appointment date and we need a method to process data from that component. Likewise, we need a and method. We don't want the date picker to show unavailable days (including ) so we need to pass a method to it. handleSetAppointmentDate() handleSetAppointmentSlot() handleSetAppointmentMeridiem() today checkDisableDate() In the lifecycle method we'll fetch our data from our backend, then handle that data with a separate method. For a fetching error, we'll need a method. componentWillMount() handleFetch() handleFetchError() Upon submitting the appointment data, we’ll use to send it to our backend. We'll need a and method for when the user is filling out contact information. handleSubmit() validateEmail() validatePhone() The user-friendly string above the form will be rendered in a separate method with . So will available appointment times and the confirmation modal with and respectively. renderConfirmationString() renderAppointmentTimes() renderAppointmentConfirmation() Finally, we’ll use a handy method to respond to the browser window changing in width resize() All in all, unwritten methods included, our now looks like this (method bindings included): App.js { Component } injectTapEventPlugin injectTapEventPlugin() { () { () .state = { : , : , : , : , : , : , : , : , : , : .innerWidth < , : } .handleNavToggle = .handleNavToggle.bind( ) .handleNextStep = .handleNextStep.bind( ) .handleSetAppointmentDate = .handleSetAppointmentDate.bind( ) .handleSetAppointmentSlot = .handleSetAppointmentSlot.bind( ) .handleSetAppointmentMeridiem = .handleSetAppointmentMeridiem.bind( ) .handleSubmit = .handleSubmit.bind( ) .validateEmail = .validateEmail.bind( ) .validatePhone = .validatePhone.bind( ) .checkDisableDate = .checkDisableDate.bind( ) .renderAppointmentTimes = .renderAppointmentTimes.bind( ) .renderConfirmationString = .renderConfirmationString.bind( ) .renderAppointmentConfirmation = .renderAppointmentConfirmation.bind( ) .resize = .resize.bind( ) } handleNavToggle() { } handleNextStep() { } handleSetAppointmentDate(date) { } handleSetAppointmentSlot(slot) { } handleSetAppointmentMeridiem(meridiem) { } handleFetch(response) { } handleFetchError(err) { } handleSubmit() { } validateEmail(email) { } validatePhone(phoneNumber) { } checkDisableDate(date) { } renderConfirmationString() { } renderAppointmentTimes() { } renderAppointmentConfirmation() { } resize() { } componentWillMount() { } componentWillUnmount() { } render() { ( ) } } // ./src/Components/App.js import from 'react' import from 'react-tap-event-plugin' export default class App extends Component constructor super this loading true navOpen false confirmationModalOpen false confirmationTextVisible false stepIndex 0 appointmentDateSelected false appointmentMeridiem 0 validEmail false validPhone false smallScreen window 768 confirmationSnackbarOpen false //method bindings this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this //lifecycle methods //fetch data from cosmic, watch window width //remove window width event listener //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 which acts as the primary toolbar AppBar A , which is opened from the 's primary button and serves as the app's navigation, following Material Design. Drawer AppBar Within the , s to display links Drawer MenuItem A as the primary content container Card A 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 is true and the last two will be disabled as long as the user hasn't filled out the previous step. Stepper state.loading Nested in the , three which contain and components. Stepper Steps StepButton StepContent In the first , we'll use a to let the user choose an appointment date. Unavailable days will be disabled according to the return value of . Selection of a date will be handled with Step DatePicker checkDisableDate() handleSetAppointmentDate() In the second , 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 for the filter and a to hold the time slot buttons. We need extra logic to render the radio buttons, so will do that in the method. That will return a set of s. Step SelectField RadioButtonGroup renderAppointmentTimes() RadioButton The last will ask the user to input their name, email address, and phone number using components. A will be used a submit button to open the confirmation . The users inputted phone number and email address will be validated with and respectively. Step TextField RaisedButton Dialog validatePhone() validateEmail() Finally, we’ll include a convenient to display notifcations about the loading state and submission at the bottom of the page. All in all, after having written out the method, our app will look like this: SnackBar render() AppBar Drawer Dialog Divider MenuItem Card DatePicker TimePicker TextField SelectField SnackBar { Step, Stepper, StepLabel, StepContent, StepButton } { RadioButton, RadioButtonGroup } RaisedButton ; FlatButton logo { render() { { stepIndex, loading, navOpen, smallScreen, confirmationModalOpen, confirmationSnackbarOpen, ...data } = .state contactFormFilled = data.firstName && data.lastName && data.phone && data.email && data.validPhone && data.validEmail modalActions = [ <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> ) } } // ./src/Components/App.js // .. previous imports import from 'material-ui/AppBar' import from 'material-ui/Drawer' import from 'material-ui/Dialog' import from 'material-ui/Divider' import from 'material-ui/MenuItem' import from 'material-ui/Card' import from 'material-ui/DatePicker' import from 'material-ui/TimePicker' import from 'material-ui/TextField' import from 'material-ui/SelectField' import from 'material-ui/Snackbar' import from 'material-ui/stepper' import from 'material-ui/RadioButton' import from 'material-ui/RaisedButton' import from 'material-ui/FlatButton' import from './../../dist/assets/logo.svg' export default class App extends Component // ... component methods, lifecycle methods const this const const this.setState({ confirmationModalOpen : false})} />, < = = = => FlatButton label "Cancel" primary {false} onClick {() 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 method. In we'll use 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. componentWillMount() componentWillMount() axios axios { () {} componentWillMount() { .series({ configs(callback) { axios.get(HOST + ).then( callback( , res.data.data) ) }, appointments(callback) { axios.get(HOST + ).then( { callback( , res.data.data) }) } }, (err,response) => { err ? .handleFetchError(err) : .handleFetch(response) }) addEventListener( , .resize) } } // ./src/Components/App.js // previous imports import async from 'async' import from 'axios' export default class App extends Component constructor async 'api/config' => res null 'api/appointments' => res null this this 'resize' this // rest... We use to make our calls in series, and name them so we have access to them as and in . We also use to start tracking the window width with . async axios response.configs response.appointments handleFetch() componentWillMount resize() componentWillUnmount() Practicing good form, we’ll remove the event listener in . componentWillUnmount() axios { () {} componentWillUnmount() { removeEventListener( , .resize) } } // ./src/Components/App.js // previous imports import async from 'async' import from 'axios' export default class App extends Component constructor 'resize' this // rest... 7. Processing Data Having fetched the data we need, we’ll process a successful fetch with and an error with . In we'll build a schedule of appointmens to store in the state, such that . We also use this method to store the app's configuration data in the state. handleFetch() handleFetchError() handleFetch() schedule = { appointmentDate: [slots] } handleFetch() handleFetch(response) { { configs, appointments } = response initSchedule = {} today = moment().startOf( ) initSchedule[today.format( )] = schedule = !appointments.length ? initSchedule : appointments.reduce( { { date, slot } = appointment dateString = moment(date, ).format( ) !currentSchedule[date] ? currentSchedule[dateString] = ( ).fill( ) : .isArray(currentSchedule[dateString]) ? currentSchedule[dateString][slot] = : currentSchedule }, initSchedule) ( day schedule) { slots = schedule[day] slots.length ? (slots.every( slot === )) ? schedule[day] = : : } .setState({ schedule, : configs.site_title, : configs.about_page_url, : configs.contact_page_url, : configs.home_page_url, : }) } const const const 'day' 'YYYY-DD-MM' true const ( ) => currentSchedule, appointment const const 'YYYY-DD-MM' 'YYYY-DD-MM' Array 8 false null Array true null return for let in let => slot true true null null this siteTitle aboutPageUrl contactPageUrl homePageUrl loading false handleFetchError() For handling errors, we’ll simply show the users an error message in the . SnackBar handleFetchError(err) { .log( + err) .setState({ : , : }) } console 'Error fetching data:' this 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() { .setState({ : ! .state.navOpen }) } return this navOpen this Then, as long as the user isn’t on the last step, we’ll handle incrementing the step. handleNextStep() { { stepIndex } = .state (stepIndex < ) ? .setState({ : stepIndex + }) : } const this return 3 this stepIndex 1 null Finally, we’ll simply change the state on resize if the window width is less than 768px. resize() { .setState({ : .innerWidth < }) } this smallScreen window 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) { .handleNextStep() .setState({ : date, : }) } handleSetAppointmentSlot(slot) { .handleNextStep() .setState({ : slot }) } handleSetAppointmentMeridiem(meridiem) { .setState({ : meridiem}) } this this appointmentDate confirmationTextVisible true this this appointmentSlot this appointmentMeridiem 10. Handle Validations We need to validate the user’s inputted email address, phone number, and we need to feed the 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. DatePicker validateEmail(email) { regex = regex.test(email) ? .setState({ : email, : }) : .setState({ : }) } validatePhone(phoneNumber) { regex = regex.test(phoneNumber) ? .setState({ : phoneNumber, : }) : .setState({ : }) } const /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i return this email validEmail true this validEmail false const /^(1\s|1|)?((\(\d{3}\))|\d{3})(\-|\s)?(\d{3})(\-|\s)?(\d{4})$/ return this phone validPhone true this validPhone false For checking if a date should be disabled, we need to check if the date passed by is either in or is today. DatePicker state.schedule checkDisableDate(day) { dateString = moment(day).format( ) .state.schedule[dateString] === || moment(day).startOf( ).diff(moment().startOf( )) < } const 'YYYY-DD-MM' return this true 'day' 'day' 0 11. Build the Render Methods for the confirmation strings and the time slot radio buttons In our lifecycle method we abstracted out the logic for displaying the dynamic confirmation string above the , the confirmation details we'll show in the confirmation modal, and the radio buttons for selecting a time slot. render() Card 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: ' return this. .confirmationTextVisible ? { Scheduling a hour </span> appointment {this. .appointmentDate && {moment(this. .appointmentDate).format('dddd[,] MMMM Do')}</span> </span>} {Number.isInteger(this. .appointmentSlot) && at {moment().hour( ).minute( ).add(this. .appointmentSlot, 'hours').format('h:mm a')}</span></span>} </span>} </h2> : null } #00bcd4'} state <h2 style={{ textAlign: this.state.smallScreen ? 'center' : 'left', color: '#bdbdbd', lineHeight: 1.5, padding: '0 10px', fontFamily: 'Roboto'}}> <span> <span style={spanStyle}> 1 state <span> on <span style={spanStyle}> state state <span> <span style={spanStyle}> 9 0 state Then, similarly, we’ll let the user verify their data before confirming submission. renderAppointmentConfirmation() { const spanStyle = { color: ' return Name: {this. .firstName} {this. .lastName}</span></p> Number: {this. .phone}</span></p> Email: {this. .email}</span></p> Appointment: {moment(this. .appointmentDate).format('dddd[,] MMMM Do[,] YYYY')}</span> at {moment().hour( ).minute( ).add(this. .appointmentSlot, 'hours').format('h:mm a')}</span></p> </section> } #00bcd4' } <section> <p> <span style={spanStyle}> state state <p> <span style={spanStyle}> state <p> <span style={spanStyle}> state <p> <span style={spanStyle}> state <span style={spanStyle}> 9 0 state 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 , for the later, we check the meridiem part with . To compute the time string in 12 hour format, we add the slot to 9AM in units of hours. state.schedule moment().format('a') renderAppointmentTimes() { (! .state.loading) { slots = [...Array( ).keys()] slots.map(slot => { appointmentDateString = moment( .state.appointmentDate).format( ) t1 = moment().hour( ).minute( ).add(slot, ) t2 = moment().hour( ).minute( ).add(slot + , ) scheduleDisabled = .state.schedule[appointmentDateString] ? .state.schedule[moment( .state.appointmentDate).format( )][slot] : meridiemDisabled = .state.appointmentMeridiem ? t1.format( ) === : t1.format( ) === <RadioButton label={t1.format( ) + + t2.format( )} key={slot} value={slot} style={{marginBottom: , display: meridiemDisabled ? : }} disabled={scheduleDisabled || meridiemDisabled}/> }) } { } } if this const 8 return const this 'YYYY-DD-MM' const 9 0 'hours' const 9 0 1 'hours' const this this this 'YYYY-DD-MM' false const this 'a' 'am' 'a' 'pm' return 'h:mm a' ' - ' 'h:mm a' 15 'none' 'inherit' 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 POST. We'll notifty them of either a success or an error. axios handleSubmit() { appointment = { : moment( .state.appointmentDate).format( ), : .state.appointmentSlot, : .state.firstName + + .state.lastName, : .state.email, : .state.phone } axios.post(HOST + , ) axios.post(HOST + , appointment) .then( .setState({ : , : , : })) .catch( { .log(err) .setState({ : , : }) }) } const date this 'YYYY-DD-MM' slot this name this ' ' this email this phone this 'api/appointments' 'api/appointments' => response this confirmationSnackbarMessage "Appointment succesfully added!" confirmationSnackbarOpen true processed true => err console return this 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. { Component } injectTapEventPlugin axios moment AppBar Drawer Dialog Divider MenuItem Card DatePicker TimePicker TextField SelectField SnackBar { Step, Stepper, StepLabel, StepContent, StepButton } { RadioButton, RadioButtonGroup } RaisedButton ; FlatButton logo injectTapEventPlugin() HOST = PRODUCTION ? : { () { () .state = { : , : , : , : , : , : , : , : , : , : .innerWidth < , : } .handleNavToggle = .handleNavToggle.bind( ) .handleNextStep = .handleNextStep.bind( ) .handleSetAppointmentDate = .handleSetAppointmentDate.bind( ) .handleSetAppointmentSlot = .handleSetAppointmentSlot.bind( ) .handleSetAppointmentMeridiem = .handleSetAppointmentMeridiem.bind( ) .handleSubmit = .handleSubmit.bind( ) .validateEmail = .validateEmail.bind( ) .validatePhone = .validatePhone.bind( ) .checkDisableDate = .checkDisableDate.bind( ) .renderAppointmentTimes = .renderAppointmentTimes.bind( ) .renderConfirmationString = .renderConfirmationString.bind( ) .renderAppointmentConfirmation = .renderAppointmentConfirmation.bind( ) .resize = .resize.bind( ) } handleNavToggle() { .setState({ : ! .state.navOpen }) } handleNextStep() { { stepIndex } = .state (stepIndex < ) ? .setState({ : stepIndex + }) : } handleSetAppointmentDate(date) { .handleNextStep() .setState({ : date, : }) } handleSetAppointmentSlot(slot) { .handleNextStep() .setState({ : slot }) } handleSetAppointmentMeridiem(meridiem) { .setState({ : meridiem}) } handleFetch(response) { { configs, appointments } = response initSchedule = {} today = moment().startOf( ) initSchedule[today.format( )] = schedule = !appointments.length ? initSchedule : appointments.reduce( { { date, slot } = appointment dateString = moment(date, ).format( ) !currentSchedule[date] ? currentSchedule[dateString] = ( ).fill( ) : .isArray(currentSchedule[dateString]) ? currentSchedule[dateString][slot] = : currentSchedule }, initSchedule) ( day schedule) { slots = schedule[day] slots.length ? (slots.every( slot === )) ? schedule[day] = : : } .setState({ schedule, : configs.site_title, : configs.about_page_url, : configs.contact_page_url, : configs.home_page_url, : }) } handleFetchError(err) { .log( + err) .setState({ : , : }) } handleSubmit() { appointment = { : moment( .state.appointmentDate).format( ), : .state.appointmentSlot, : .state.firstName + + .state.lastName, : .state.email, : .state.phone } axios.post(HOST + , ) axios.post(HOST + , appointment) .then( .setState({ : , : , : })) .catch( { .log(err) .setState({ : , : }) }) } validateEmail(email) { regex = regex.test(email) ? .setState({ : email, : }) : .setState({ : }) } validatePhone(phoneNumber) { regex = regex.test(phoneNumber) ? .setState({ : phoneNumber, : }) : .setState({ : }) } checkDisableDate(day) { dateString = moment(day).format( ) .state.schedule[dateString] === || moment(day).startOf( ).diff(moment().startOf( )) < } renderConfirmationString() { spanStyle = { : } .state.confirmationTextVisible ? <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> : } renderAppointmentTimes() { (! .state.loading) { slots = [...Array( ).keys()] slots.map( { appointmentDateString = moment( .state.appointmentDate).format( ) t1 = moment().hour( ).minute( ).add(slot, ) t2 = moment().hour( ).minute( ).add(slot + , ) scheduleDisabled = .state.schedule[appointmentDateString] ? .state.schedule[moment( .state.appointmentDate).format( )][slot] : meridiemDisabled = .state.appointmentMeridiem ? t1.format( ) === : t1.format( ) === <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> <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> ) } } // ./src/Components/App.js import from 'react' import from 'react-tap-event-plugin' import from 'axios' import async from 'async' import from 'moment' import from 'material-ui/AppBar' import from 'material-ui/Drawer' import from 'material-ui/Dialog' import from 'material-ui/Divider' import from 'material-ui/MenuItem' import from 'material-ui/Card' import from 'material-ui/DatePicker' import from 'material-ui/TimePicker' import from 'material-ui/TextField' import from 'material-ui/SelectField' import from 'material-ui/Snackbar' import from 'material-ui/stepper' import from 'material-ui/RadioButton' import from 'material-ui/RaisedButton' import from 'material-ui/FlatButton' import from './../../dist/assets/logo.svg' const '/' 'http://localhost:3000/' export default class App extends Component constructor super this loading true navOpen false confirmationModalOpen false confirmationTextVisible false stepIndex 0 appointmentDateSelected false appointmentMeridiem 0 validEmail true validPhone true smallScreen window 768 confirmationSnackbarOpen false this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this return this navOpen this const this return 3 this stepIndex 1 null this this appointmentDate confirmationTextVisible true this this appointmentSlot this appointmentMeridiem const const const 'day' 'YYYY-DD-MM' true const ( ) => currentSchedule, appointment const const 'YYYY-DD-MM' 'YYYY-DD-MM' Array 8 false null Array true null return for let in let => slot true true null null this siteTitle aboutPageUrl contactPageUrl homePageUrl loading false console 'Error fetching data:' this confirmationSnackbarMessage 'Error fetching data' confirmationSnackbarOpen true const date this 'YYYY-DD-MM' slot this name this ' ' this email this phone this 'api/appointments' 'api/appointments' => response this confirmationSnackbarMessage "Appointment succesfully added!" confirmationSnackbarOpen true processed true => err console return this confirmationSnackbarMessage "Appointment failed to save." confirmationSnackbarOpen true const /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i return this email validEmail true this validEmail false const /^(1\s|1|)?((\(\d{3}\))|\d{3})(\-|\s)?(\d{3})(\-|\s)?(\d{4})$/ return this phone validPhone true this validPhone false const 'YYYY-DD-MM' return this true 'day' 'day' 0 const color '#00bcd4' return this { < = ? ' ' ' ', '# ', , ' ', ' '}}> h2 style {{ textAlign: this.state.smallScreen center : left color: bdbdbd lineHeight: 1.5 padding: 0 10px fontFamily: Roboto } </ > h2 null if this const 8 return => slot const this 'YYYY-DD-MM' const 9 0 'hours' const 9 0 1 'hours' const this this this 'YYYY-DD-MM' false const this 'a' 'am' 'a' 'pm' return }) } else { return null } } renderAppointmentConfirmation() { const spanStyle = { color: '#00bcd4' } 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 } resize() { this.setState({ smallScreen: window.innerWidth 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 = [ < }) } () { ({ ( ) { ( + ' / ') ( => 768 componentWillMount async.series configs callback axios.get HOST api config .then res < = > { Component } injectTapEventPlugin axios async moment AppBar Drawer Dialog Divider MenuItem Card DatePicker TimePicker TextField SelectField SnackBar { Step, Stepper, StepLabel, StepContent, StepButton } { RadioButton, RadioButtonGroup } RaisedButton ; FlatButton logo </ > code class "language-markup" import from 'react' import from 'react-tap-event-plugin' import from 'axios' import from 'async' import from 'moment' import from 'material-ui/AppBar' import from 'material-ui/Drawer' import from 'material-ui/Dialog' import from 'material-ui/Divider' import from 'material-ui/MenuItem' import from 'material-ui/Card' import from 'material-ui/DatePicker' import from 'material-ui/TimePicker' import from 'material-ui/TextField' import from 'material-ui/SelectField' import from 'material-ui/Snackbar' import from 'material-ui/stepper' import from 'material-ui/RadioButton' import from 'material-ui/RaisedButton' import from 'material-ui/FlatButton' import from './../../dist/assets/logo.svg' code < = >injectTapEventPlugin() const HOST = PRODUCTION ? : </ > code class "language-markup" '/' 'http://localhost:3000/' code <code = >export { constructor() { () .state = { loading: , navOpen: , confirmationModalOpen: , confirmationTextVisible: , stepIndex: , appointmentDateSelected: , appointmentMeridiem: , validEmail: , validPhone: , smallScreen: window.innerWidth &lt; , confirmationSnackbarOpen: }</code> class "language-markup" default class App extends Component super this true false false false 0 false 0 true true 768 false <code ( ) .handleNextStep = .handleNextStep.bind( ) .handleSetAppointmentDate = .handleSetAppointmentDate.bind( ) .handleSetAppointmentSlot = .handleSetAppointmentSlot.bind( ) .handleSetAppointmentMeridiem = .handleSetAppointmentMeridiem.bind( ) .handleSubmit = .handleSubmit.bind( ) .validateEmail = .validateEmail.bind( ) .validatePhone = .validatePhone.bind( ) .checkDisableDate = .checkDisableDate.bind( ) .renderAppointmentTimes = .renderAppointmentTimes.bind( ) .renderConfirmationString = .renderConfirmationString.bind( ) .renderAppointmentConfirmation = .renderAppointmentConfirmation.bind( ) .resize = .resize.bind( ) }</code> =" - "> . = . . class language markup this handleNavToggle this handleNavToggle bind this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this this <code () { .setState({ navOpen: ! .state.navOpen }) }</code> =" - "> class language markup handleNavToggle return this this <code () { { stepIndex } = .state (stepIndex &lt; ) ? .setState({ stepIndex: stepIndex + }) : }</code> =" - "> class language markup handleNextStep const this return 3 this 1 null <code = > handle { this.handle this.set }</code> class "language-markup" SetAppointmentDate( ) date NextStep() State({ : , : }) appointmentDate date confirmationTextVisible true <code = > handle { this.handle this.set }</code> class "language-markup" SetAppointmentSlot( ) slot NextStep() State({ : }) appointmentSlot slot <code = > handle { this.set }</code> class "language-markup" SetAppointmentMeridiem( ) meridiem State({ : }) appointmentMeridiem meridiem <code = > handleFetch(response) { { configs, appointments } = response initSchedule = {} today = moment().startOf( ) initSchedule[today.format( )] = schedule = !appointments.length ? initSchedule : appointments.reduce((currentSchedule, appointment) =&gt; { { date, slot } = appointment dateString = moment(date, ).format( ) !currentSchedule[date] ? currentSchedule[dateString] = ( ).fill( ) : .isArray(currentSchedule[dateString]) ? currentSchedule[dateString][slot] = : currentSchedule }, initSchedule)< class "language-markup" const const const 'day' 'YYYY-DD-MM' true const const const 'YYYY-DD-MM' 'YYYY-DD-MM' Array 8 false null Array true null return /code> <code = > ( schedule) { slots = schedule[ ] slots.length ? (slots.every(slot =&gt; slot === )) ? schedule[ ] = : : }</code> class "language-markup" for let day in let day true day true null null < class= > this.setState({ schedule, siteTitle: configs , aboutPageUrl: configs , contactPageUrl: configs , homePageUrl: configs , loading: false }) }</code> code "language-markup" .site_title .about_page_url .contact_page_url .home_page_url <code = > handle { console.log('Error fetching data:' + err) this.set }</code> class "language-markup" FetchError( ) err State({ : 'Error ', : }) confirmationSnackbarMessage fetching data confirmationSnackbarOpen true handleSubmit() { const appointment = { date: moment(this. .appointmentDate).format('YYYY-DD-MM'), slot: this. .appointmentSlot, name: this. .firstName + ' ' + this. .lastName, email: this. .email, phone: this. .phone } axios.post(HOST + 'api/appointments', ) axios.post(HOST + 'api/appointments', appointment) .then(response =&gt; this. State({ confirmationSnackbarMessage: , confirmationSnackbarOpen: true, processed: true })) .catch(err =&gt; { console. (err) return this. State({ confirmationSnackbarMessage: , confirmationSnackbarOpen: true }) }) }</code> <code class="language-markup"> state state state state state state set "Appointment succesfully added!" log set "Appointment failed to save." <code class="language-markup"> validateEmail(email) { const regex = /^(([^&lt;&gt;() ,;: @ ]+( [^&lt;&gt;() ,;: @ ]+)*)|( .+ ))@(([^&lt;&gt;()[ ,;: @ ]+ )+[^&lt;&gt;()[ ,;: @ ]{2,})$/i return regex.test(email) ? this.setState({ email: email, validEmail: true }) : this.setState({ validEmail: false }) }</code> \[ \] \. \s \" \. \[ \] \. \s \" \" \" \] \. \s \" \. \] \. \s \" <code class="language-markup"> validatePhone(phoneNumber) { const regex = /^(1 |1|)?(( )| )( | )?( )( | )?( ) \ s \ ( \ 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 (day) { dateString = moment(day).format( ) .state.schedule[dateString] === || moment(day).startOf( ).diff(moment().startOf( )) &lt; }</code> =" - "> class language markup checkDisableDate const 'YYYY-DD-MM' return this true 'day' 'day' 0 <code () { spanStyle = {color: } .state.confirmationTextVisible ? &lt;h2 style={{ textAlign: .state.smallScreen ? : , color: , lineHeight: , padding: , fontFamily: }}&gt; { &lt;span&gt; Scheduling a</code> =" - "> class language markup renderConfirmationString const '#00bcd4' return this this 'center' 'left' '#bdbdbd' 1.5 '0 10px' 'Roboto' ;span style={spanStyle} ; hour ;/span ; <code class="language-markup"> &lt &gt 1 &lt &gt </code> <code class= > appointment {this.state.appointmentDate ; ; ;span ; ;span style={spanStyle} ;{moment(this.state.appointmentDate). ( )} ;/span ; ;/span ;} {Number.isInteger(this.state.appointmentSlot) ; ; ;span ;at ;span style={spanStyle} ;{moment() 9) 0). (this.state.appointmentSlot, ). ( )} ;/span ; ;/span ;} ;/span ;} ;/h2 ; : }</code> "language-markup" &amp &amp &lt &gt on &lt &gt format 'dddd[,] MMMM Do' &lt &gt &lt &gt &amp &amp &lt &gt &lt &gt .hour( .minute( add 'hours' format 'h:mm a' &lt &gt &lt &gt &lt &gt &lt &gt null <code () { (! .state.loading) { slots = [...Array( ).keys()] slots.map(slot =&gt; { appointmentDateString = moment( .state.appointmentDate).format( ) t1 = moment().hour( ).minute( ).add(slot, ) t2 = moment().hour( ).minute( ).add(slot + , ) scheduleDisabled = .state.schedule[appointmentDateString] ? .state.schedule[moment( .state.appointmentDate).format( )][slot] : meridiemDisabled = .state.appointmentMeridiem ? t1.format( ) === : t1.format( ) === &lt;RadioButton label={t1.format( ) + + t2.format( )} key={slot} value={slot} style={{marginBottom: , display: meridiemDisabled ? : }} disabled={scheduleDisabled || meridiemDisabled}/&gt; }) } { } }</code> =" - "> class language markup renderAppointmentTimes if this const 8 return const this 'YYYY-DD-MM' const 9 0 'hours' const 9 0 1 'hours' const this this this 'YYYY-DD-MM' false const this 'a' 'am' 'a' 'pm' return 'h:mm a' ' - ' 'h:mm a' 15 'none' 'inherit' else return null <code class= > renderAppointmentConfirmati ) { const spanStyle = { color: } ;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). ( )} ;/span ; at ;span style={spanStyle} ;{moment() 9) 0). (this.state.appointmentSlot, ). ( )} ;/span ; ;/p ; ;/section ; }</code> "language-markup" on( '#00bcd4' return &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt &lt &gt format 'dddd[,] MMMM Do[,] YYYY' &lt &gt &lt &gt .hour( .minute( add 'hours' format 'h:mm a' &lt &gt &lt &gt &lt &gt <code = > resize { this.set }</code> class "language-markup" () State({ : . & ; 768 }) smallScreen window innerWidth lt <code () { async.series({ configs(callback) { axios. (HOST + ).then(res =&gt; callback( , res. . ) ) }, appointments(callback) { axios. (HOST + ).then(res =&gt; { callback( , res. . ) }) } }, (err,response) =&gt; { err ? .handleFetchError(err) : .handleFetch(response) }) addEventListener( , .resize) }</code> =" - "> class language markup componentWillMount get 'api/config' null data data get 'api/appointments' null data data this this 'resize' this <code = > component { remove }</code> class "language-markup" WillUnmount() EventListener(' ', . ) resize this resize <code class= > 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 = ={false} onClick={() = ; this.setState({ confirmationModalOpen : false})} / ;, ;FlatButton = ={true} onClick={() = ; this.handleSubmit()} / ; ] ( ;div ; ;AppBar ={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: , transform: }}/ ; ;a style={{textDecoration: }} href={this.state.homePageUrl} ; ;MenuItem ;Home ;/MenuItem ; ;/a ; ;a style={{textDecoration: }} href={this.state.aboutPageUrl} ; ;MenuItem ;About ;/MenuItem ; ;/a ; ;a style={{textDecoration: }} href={this.state.contactPageUrl} ; ;MenuItem ;Contact ;/MenuItem ; ;/a ;</code> "language-markup" &amp &amp &amp &amp &amp &amp &amp &amp &amp &amp &lt label "Cancel" primary &gt &gt &lt label "Confirm" primary &gt &gt return &lt &gt &lt title &gt &gt &lt &gt &gt &lt '50%' 'translateX(-50%)' &gt &lt 'none' &gt &lt &gt &lt &gt &lt &gt &lt 'none' &gt &lt &gt &lt &gt &lt &gt &lt 'none' &gt &lt &gt &lt &gt &lt &gt & ;MenuItem { } {{ marginLeft: ' %', transform: 'translate(- %)' }}& ; { + moment().format('YYYY')}& ;/MenuItem& ; & ;/Drawer& ; & ;section {{ maxWidth: !smallScreen ? ' %' : ' %', margin: 'auto', marginTop: !smallScreen ? : , }}& ; {this.renderConfirmationString()} & ;Card {{ padding: ' px px px px', height: smallScreen ? ' vh' : null }}& ; & ;Stepper {stepIndex} { } & ; & ;Step {loading}& ; & ;StepButton {() =& ; this.setState({ stepIndex: })}& ; Choose an available day for your appointment & ;/StepButton& ; & ;StepContent& ; & ;DatePicker {{ marginTop: , marginLeft: }} {data.appointmentDate} {smallScreen ? 'portrait' : 'landscape'} {(n, ) =& ; this.handleSetAppointmentDate( )} {day =& ; this.checkDisableDate(day)} /& ; & ;/StepContent& ; & ;/Step& ; & ;Step { !data.appointmentDate }& ; & ;StepButton {() =& ; this.setState({ stepIndex: })}& ; Choose an available time for your appointment & ;/StepButton& ; & ;StepContent& ; & ;SelectField {data.appointmentMeridiem} {(evt, key, payload) =& ; this.handleSetAppointmentMeridiem(payload)} {value =& ; value ? 'PM' : 'AM'}& ; & ;MenuItem { }& ;AM& ;/MenuItem& ; & ;MenuItem { }& ;PM& ;/MenuItem& ; & ;/SelectField& ; & ;RadioButtonGroup {{ marginTop: , marginLeft: }} {data.appointmentSlot} {(evt, val) =& ; this.handleSetAppointmentSlot(val)}& ; {this.renderAppointmentTimes()} & ;/RadioButtonGroup& ; & ;/StepContent& ; & ;/Step& ; & ;Step { ! .isInteger(this.state.appointmentSlot) }& ; & ;StepButton {() =& ; this.setState({ stepIndex: })}& ; Share your contact ormation with us we'll send you a reminder & ;/StepButton& ; & ;StepContent& ; & ;section& ; & ;TextField {{ display: 'block' }} {(evt, newValue) =& ; this.setState({ firstName: newValue })}/& ; & ;TextField {{ display: 'block' }} {(evt, newValue) =& ; this.setState({ lastName: newValue })}/& ; & ;TextField {{ display: 'block' }} {data.validEmail ? null : 'Enter a valid email address'} {(evt, newValue) =& ; this.validateEmail(newValue)}/& ; & ;TextField {{ display: 'block' }} {data.validPhone ? null: 'Enter a valid phone '} {(evt, newValue) =& ; this.validatePhone(newValue)} /& ; & ;RaisedButton {{ display: 'block' }} {contactFormFilled ? 'Schedule' : 'Fill out your ormation to schedule'} { } { } {() =& ; this.setState({ confirmationModalOpen: !this.state.confirmationModalOpen })} {!contactFormFilled || data.processed } {{ marginTop: , maxWidth: }} /& ; & ;/section& ; & ;/StepContent& ; & ;/Step& ; & ;/Stepper& ; & ;/Card& ; & ;Dialog { } {confirmationModalOpen} {modalActions} & ; {this.renderAppointmentConfirmation()} & ;/Dialog& ; & ;SnackBar {confirmationSnackbarOpen || loading} {loading ? 'Loading... ' : data.confirmationSnackbarMessage || ''} { } {() =& ; this.setState({ confirmationSnackbarOpen: })} /& ; & ;/section& ; & ;/div& ; ) } } <code class="language-markup"> lt disabled= true style= 50 50 gt "© Copyright " lt gt lt gt lt style= 80 100 20 0 gt lt style= 10 10 25 10 100 gt lt activeStep= linear= false orientation= "vertical" gt lt disabled= gt lt onClick= gt 0 gt lt gt lt gt lt style= 10 10 value= hintText= "Select a date" mode= onChange= date gt date shouldDisableDate= gt gt lt gt lt gt lt disabled= gt lt onClick= gt 1 gt lt gt lt gt lt floatingLabelText= "AM or PM" value= onChange= gt selectionRenderer= gt gt lt value= 0 gt lt gt lt value= 1 gt lt gt lt gt lt style= 15 15 name= "appointmentTimes" defaultSelected= onChange= gt gt lt gt lt gt lt gt lt disabled= Number gt lt onClick= gt 2 gt inf and lt gt lt gt lt gt lt style= name= "first_name" hintText= "First Name" floatingLabelText= "First Name" onChange= gt gt lt style= name= "last_name" hintText= "Last Name" floatingLabelText= "Last Name" onChange= gt gt lt style= name= "email" hintText= "name@mail.com" floatingLabelText= "Email" errorText= onChange= gt gt lt style= name= "phone" hintText= "(888) 888-8888" floatingLabelText= "Phone" errorText= number onChange= gt gt lt style= label= inf labelPosition= "before" primary= true fullWidth= true onClick= gt disabled= style= 20 100 gt lt gt lt gt lt gt lt gt lt gt lt modal= true open= actions= title= "Confirm your appointment" gt lt gt lt open= message= autoHideDuration= 10000 onRequestClose= gt false gt lt gt lt 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 will be where we serve our built frontend from and will hide . public .gitignore node_modules node_modules # .gitignore We’ll use the following packages: for pushing Appointment objects to Cosmic axios , , , , , and for server-ware body-parser cors http morgan path express - the official client, for fetching objects from Cosmic cosmicjs - the official client, for sending confirmation texts twilio for parsing times moment Run , then edit with a start script so we can deploy on Cosmic. yarn init package.json { // etc... : { : } } "scripts" "start" "node app.js" Then, before we start working: yarn axios body- cors cosmicjs express express- http moment morgan twilio add parser session path 2. Outline the Backend’s Structure Our Express app will be fairly basic as far as configuration and middleware goes. For submissions we'll handle post requests at . We'll serve our site configs and appointments from and respectively. Finally, since our Frontend is an SPA, we'll serve from and redirect all other requests there. Appointment /api/appointments /api/config /api/appointments index.html / Before getting into much logic, our server will start off looking like this: express = ( ) path = ( ) morgan = ( ) bodyParser = ( ) cors = ( ) config = ( ) http = ( ) Cosmic = ( ) twilio = ( ) moment = ( ) axios = ( ) config = { : { : process.env.COSMIC_BUCKET, : process.env.COSMIC_READ_KEY, : process.env.COSMIC_WRITE_KEY }, : { : process.env.TWILIO_AUTH, : process.env.TWILIO_SID, : process.env.TWILIO_NUMBER } } app = express() env = process.env.NODE_ENV || twilioSid = config.twilio.sid twilioAuth = config.twilio.auth twilioClient = twilio(twilioSid, twilioAuth) twilioNumber = config.twilio.number app.set( , ) app.use(session({ : , : , : , : { : } })) app.use(cors()) app.use(morgan( )) app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ : })) app.use(express.static(path.join(__dirname, ))) app.set( , process.env.PORT || ) app.post( , (req, res) => { }) app.get( , (req, res) => { }) app.get( , (req, res) => { }) app.get( , (req, res) => { res.send( ) }) app.get( , (req, res) => { res.redirect( ) }) http.createServer(app).listen(app.get( ), () => .log( + app.get( )) ) const require 'express' const require 'path' const require 'morgan' const require 'body-parser' const require 'cors' const require './config' const require 'http' const require 'cosmicjs' const require 'twilio' const require 'moment' const require 'axios' const bucket slug read_key write_key twilio auth sid number const const 'development' const const const const 'trust proxy' 1 secret 'sjcimsoc' resave false saveUninitialized true cookie secure false 'dev' extended true 'public' 'port' 3000 '/api/appointments' //handle posting new appointments to Cosmic //and sending a confirmation text with Twilio '/api/config' //fetch configs from Cosmic, expose to frontend '/api/appointments' //fetch appointments from Cosmic, expose to frontend without personal data '/' 'index.html' '*' '/' 'port' console 'Server running at: ' 'port' Note: we’ll provide all variables at deployment with Cosmic. Cosmic-specific variables are supplied automatically. process.env 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 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. axios We have: app.post( , (req, res) => { appointment = req.body appointment.phone = appointment.phone.replace( , ) date = moment(appointment.date, ).startOf( ) time = date.hour( ).add(appointment.slot, ) smsBody = twilioClient.messages.create({ : + appointment.phone, : twilioNumber, : smsBody }, (err, message) => .log(message, err)) cosmicObject = { : appointment.name, : , : config.bucket.write_key, : [ { : , : , : date.format( ) }, { : , : , : appointment.slot }, { : , : , : appointment.email },{ : , : , : appointment.phone } ] } axios.post( , cosmicObject) .then( res.json({ : })).catch( res.json({ : })) }) '/api/appointments' const /\D/g '' const 'YYYY-DD-MM' 'day' const 9 'hours' const ` , this message is to confirm your appointment at on .` ${appointment.name} ${time.format( )} 'h:mm a' ${date.format( )} 'dddd MMMM Do[,] YYYY' //send confirmation message to user to '+1' from body console //push to cosmic const "title" "type_slug" "appointments" "write_key" "metafields" "key" "date" "type" "text" "value" 'YYYY-DD-MM' "key" "slot" "type" "text" "value" "key" "email" "type" "text" "value" "key" "phone" "type" "text" "value" //which is now stripped of all non-digits `https://api.cosmicjs.com/v1/ /add-object` ${config.bucket.slug} => response data 'success' => err data 'error ' 4. Expose Site Configs We’ll simply use to get the object we need for the frontend to display links in the navigation. cosmicjs site-config app.get( , (req,res) => { Cosmic.getObject(config, { : }, (err, response) => { data = response.object.metadata err ? res.status( ).json({ : }) : res.json({ data }) }) }) '/api/config' slug 'site-config' const 500 data 'error' 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 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 to fetch all objects, but only expose an array of objects with the form . Appointment cosmicjs Appointment { date, slot } app.get( , (req, res) => { Cosmic.getObjectType(config, { : }, (err, response) => { appointments = response.objects.all ? response.objects.all.map( { { : appointment.metadata.date, : appointment.metadata.slot } }) : {} res.json({ : appointments }) }) }) '/api/appointments' type_slug 'appointments' const => appointment return date slot data 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. express = ( ) path = ( ) morgan = ( ) bodyParser = ( ) cors = ( ) config = ( ) http = ( ) Cosmic = ( ) twilio = ( ) moment = ( ) axios = ( ) config = { : { : process.env.COSMIC_BUCKET, : process.env.COSMIC_READ_KEY, : process.env.COSMIC_WRITE_KEY }, : { : process.env.TWILIO_AUTH, : process.env.TWILIO_SID, : process.env.TWILIO_NUMBER } } app = express() env = process.env.NODE_ENV || twilioSid = config.twilio.sid twilioAuth = config.twilio.auth twilioClient = twilio(twilioSid, twilioAuth) twilioNumber = config.twilio.number app.set( , ) app.use(session({ : , : , : , : { : } })) app.use(cors()) app.use(morgan( )) app.use(bodyParser.json()) app.use(bodyParser.urlencoded({ : })) app.use(express.static(path.join(__dirname, ))) app.set( , process.env.PORT || ) app.post( , (req, res) => { appointment = req.body appointment.phone = appointment.phone.replace( , ) date = moment(appointment.date, ).startOf( ) time = date.hour( ).add(appointment.slot, ) smsBody = twilioClient.messages.create({ : + appointment.phone, : twilioNumber, : smsBody }, (err, message) => .log(message, err)) cosmicObject = { : appointment.name, : , : config.bucket.write_key, : [ { : , : , : date.format( ) }, { : , : , : appointment.slot }, { : , : , : appointment.email },{ : , : , : appointment.phone } ] } axios.post( , cosmicObject) .then( res.json({ : })).catch( res.json({ : })) }) app.get( , (req,res) => { Cosmic.getObject(config, { : }, (err, response) => { data = response.object.metadata err ? res.status( ).json({ : }) : res.json({ data }) }) }) app.get( , (req, res) => { Cosmic.getObjectType(config, { : }, (err, response) => { appointments = response.objects.all ? response.objects.all.map( { { : appointment.metadata.date, : appointment.metadata.slot } }) : {} res.json({ : appointments }) }) }) app.get( , (req, res) => { res.send( ) }) app.get( , (req, res) => { res.redirect( ) }) http.createServer(app).listen(app.get( ), () => .log( + app.get( )) ) const require 'express' const require 'path' const require 'morgan' const require 'body-parser' const require 'cors' const require './config' const require 'http' const require 'cosmicjs' const require 'twilio' const require 'moment' const require 'axios' const bucket slug read_key write_key twilio auth sid number const const 'development' const const const const 'trust proxy' 1 secret 'sjcimsoc' resave false saveUninitialized true cookie secure false 'dev' extended true 'public' 'port' 3000 '/api/appointments' const /\D/g '' const 'YYYY-DD-MM' 'day' const 9 'hours' const ` , this message is to confirm your appointment at on .` ${appointment.name} ${time.format( )} 'h:mm a' ${date.format( )} 'dddd MMMM Do[,] YYYY' //send confirmation message to user to '+1' from body console //push to cosmic const "title" "type_slug" "appointments" "write_key" "metafields" "key" "date" "type" "text" "value" 'YYYY-DD-MM' "key" "slot" "type" "text" "value" "key" "email" "type" "text" "value" "key" "phone" "type" "text" "value" //which is now stripped of all non-digits `https://api.cosmicjs.com/v1/ /add-object` ${config.bucket.slug} => response data 'success' => err data 'error ' '/api/config' slug 'site-config' const 500 data 'error' '/api/appointments' type_slug 'appointments' const => appointment return date slot data '/' 'index.html' '*' '/' 'port' console 'Server running at: ' '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, , run to build out into . Then move the contents of to the backend's public folder - . The that Webpack builds will then be the we serve from . appointment-scheduler webpack dist dist AppointmentScheduler/public index.html index.html / From , commit the app to a new Github repo. Then, create a trial Twilio account and within the Cosmic JS dashboard, add the following variables from the deploy menu. AppointmentScheduler env - your Twilio auth key TWILIO_AUTH - your Twilio sid TWILIO_SID - the phone number you have associated with your Twilio trial. TWILIO_NUMBER 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 directory, run , and create the following project structure. appointment-scheduler-extension yarn init appointment-scheduler-extension | | | | | | | | | | | | | | | | --dist --src . --Components . . --App.js . --index.html . --index.js --.babelrc --.gitignore --package.json --webpack.config.js Use the same exact template we used for the frontend. index.html Appointment Scheduler <!-- ./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 </ > title </ > head < > body < = > div id "root" </ > div </ > body </ > html We’ll be using almost all of the same packages as the frontend. Refer to Part 1 for installations, including adding and . We'll use for filtering data and as a convenient way to get our Cosmic keys, which Cosmic supplies as url parameters. lodash query-string lodash query-string Likewise, , , and will all be entirely the same as in Part 1. webpack.config.js .gitignore .babelrc won't change either besides a new scheme for config variables: index.js React ‘react’ ReactDom ‘react-dom’ App ‘./Components/App’ MuiThemeProvider ‘material-ui/styles/MuiThemeProvider’ QueryString ‘query-string’ ‘normalize.css’ .React = React url = QueryString.parse(location.search) config = { : { : url.bucket_slug, : url.write_key, : url.read_key } } ReactDom.render( <App config={config}/> </MuiThemeProvider> import from import from import from import from import from import window const const bucket slug write_key read_key < > 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: { } from t' injectTapEventPlugin from -tap-event-plugin' injectTapEventPlugin() export { constructor(props) { (props) } render() { ( ) } } import Component 'reac import 'react default class App extends Component super // set initial state // bind component methods // component methods, lifecycle methods return //Material UI components Thinking about what we need for an initial state: We’re getting our Cosmic config variables from url parametes in and we're passing them to as props, so we'll need to move those to it's state. index.js App We’ll be using a like we did in the frontend so we need to keep track of its state and message. We'll also be using a and need a similar strategy. SnackBar DatePicker 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 to a state variable when the first is selected, for the latter. 1 0 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 rows being selected. all Thus, for an initial state, we have: .state = { : props.config, : , : , : , : {}, : {}, : , : [], : , : } this 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 and handle it in a seperate (and it's companion ) componentWillMount() handleFetchMethod handleFetchError() Change the state when the filter option is changed with handleToobarDropdownChange() Override default Material UI selections with Table handleRowSelection() Handle deleting objects with Appointment handleDelete() Feed disabled dates to the with DatePicker checkDisableDate() Filter appointments with filterAppointments() Render appointments as s with TableRow setTableChildren() Including those in our extension’s skeleton, we now have: import { Component } from 'react' import injectTapEventPlugin from 'react-tap-event-plugin' inject export default App extends Component { constructor(props) { super(props) this.state = { config: props.config, snackbarDisabled: , snackbarMessage: 'Loading...', toolbarDropdownValue: , appointments: {}, filteredAppointments: {}, datePickerDisabled: , selectedRows: , deleteButtonDisabled: , allRowsSelected: } } handle { } handle { } handle { } handle { } handle { } check { } filter { } set { } component { } render { return ( ) } } TapEventPlugin() class false 1 true [] true false FetchError( ) err //handle errors fetching data from Cosmic JS Fetch( ) response //process data fetched from Cosmic JS ToolbarDropdownChange( ) val // set the dropdown value and clear filteredAppointments() if // "List All" is selected. (State 1). RowSelection( ) 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. Delete( ) selectedRows //send a post request to Cosmic JS's api to get rid of unwanted appointments DisableDate( ) date //feed the DatePicker days based on availability determined by appointments //retrieved from Cosmic Appointments( ) date //Only show appointments occuring on date TableChildren( = . . , = . . ) selectedRows this state selectedRows appointments this state appointments //render a TableRow for each appointment loaded WillMount() //fetch data immediately () //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} > ID Name Email Phone Date Time < > TableHeader < > TableRow < > TableHeaderColumn </ > TableHeaderColumn < > TableHeaderColumn </ > TableHeaderColumn < > TableHeaderColumn </ > TableHeaderColumn < > TableHeaderColumn </ > TableHeaderColumn < > TableHeaderColumn </ > TableHeaderColumn < > TableHeaderColumn </ > 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 to handle the fetch. We'll do this in . cosmicjs componentWillMount() componentWillMount() { Cosmic.getObjectType( .state.config, { : }, (err, response) => err ? .handleFetchError(err) : .handleFetch(response) ) } this type_slug 'appointments' this this We’ll handle errors with , which will show the user that an error occured in the . handleFetchError() SnackBar handleFetchError(err) { .log(err) .setState({ : }) } console this snackbarMessage 'Error loading data' If data is successfully returned, we’ll process it with . handleFetch() handleFetch(response) { appointments = response.objects.all ? response.objects.all.reduce( { date = appointment.metadata.date (!currentAppointments[date]) currentAppointments[date] = [] appointmentData = { : appointment.metadata.slot, : appointment.title, : appointment.metadata.email, : appointment.metadata.phone, : appointment.slug } currentAppointments[date].push(appointmentData) currentAppointments[date].sort( a.slot - b.slot) currentAppointments }, {}) : {} .setState({ appointments, : , : .setTableChildren([], appointments) }) } const ( ) => currentAppointments, appointment const if const slot name email phone slug ( ) => a,b return this snackbarDisabled true tableChildren this From the array of objects our bucket sends, we create a schedule of all loaded appointments, appointments. We then save that to the state and pass it to to use in rendering the . Appointment setTableChildren() 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 . Starting with handling the dropdown filter, maps to filtering the appointments by date, maps to listing all. For listing all, we reset . DatePicker 0 1 state.filteredAppointments handleToolbarDropdownChange(val) { val ? .setState({ : {}, : , : }) : .setState({ : , : }) } //0: filter by date, 1: list all this filteredAppointments datePickerDisabled true toolbarDropdownValue 1 this 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) { allRows = [...Array( .state.tableChildren.length).keys()] allRowsSelected = rowsToSelect === selectedRows = .isArray(rowsToSelect) ? rowsToSelect : allRowsSelected ? allRows : [] appointments = _.isEmpty( .state.filteredAppointments) ? .state.appointments : .state.filteredAppointments deleteButtonDisabled = selectedRows.length == tableChildren = allRowsSelected ? .setTableChildren([], appointments) : .setTableChildren(selectedRows, appointments) .setState({ selectedRows, deleteButtonDisabled, tableChildren }) } const this const 'all' const Array const this this this const 0 const this this this For disabling dates, we only make them active if , where , exists. state.appointments.date date = 'YYYY-DD-MM' checkDisableDate(day) { ! .state.appointments[moment(day).format( )] } return this 'YYYY-DD-MM' 8. Filtering Appointments and Rendering the Table When the user changes the filter dropdown to they then pick a date from the date picker. Upon choosing a date, the date picker fires to set to the sub-schedule and pass that sub-schedule . Filter By Date filterAppointments() state.filteredAppoitments state.appointments[selectedDate] setTableChildren() filterAppointments(date) { dateString = moment(date).format( ) filteredAppointments = {} filteredAppointments[dateString] = .state.appointments[dateString] .setState({ filteredAppointments, : .setTableChildren([], filteredAppointments) }) } const 'YYYY-DD-MM' const this this tableChildren this When (or any other method) calls we can optionally pass an array of selected rows and an object or let it default to and . If the appointments are filtered, we sort them by time before rendering. filterAppointments() setTableChildren() appointments state.selectedRows state.appointments setTableChildren(selectedRows = .state.selectedRows, appointments = .state.appointments) { renderAppointment = { { name, email, phone, slot } = appointment rowSelected = selectedRows.includes(index) <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> } appointmentsAreFiltered = !_.isEmpty( .state.filteredAppointments) schedule = appointmentsAreFiltered ? .state.filteredAppointments : appointments els = [] counter = appointmentsAreFiltered ? .keys(schedule).forEach( { schedule[date].forEach( els.push(renderAppointment(date, appointment, index))) }) : .keys(schedule).sort( moment(a, ).isBefore(moment(b, ))) .forEach( { schedule[date].forEach( { els.push(renderAppointment(date, appointment, counter)) counter++ }) }) els } this this const ( ) => date, appointment, index const const return < = = > TableRow key {index} selected {rowSelected} </ > TableRow const this const this const let 0 Object => date ( ) => appointment, index Object ( ) => a,b 'YYYY-DD-MM' 'YYYY-MM-DD' ( ) => date, index => appointment return 9. Deleting Appointments The last thing we need to handle is letting the user delete appointments, leveraging for deleting objects found using from according to . cosmicjs lodash state.appointments selectedRows handleDelete(selectedRows) { { config } = .state selectedRows.map( { { tableChildren, appointments } = .state date = moment(tableChildren[row].props.children[ ].props.children, ).format( ) slot = moment(tableChildren[row].props.children[ ].props.children, ).diff(moment().hours( ).minutes( ).seconds( ), ) + _.find(appointments[date], appointment => appointment.slot === slot ) }).map( appointment.slug).forEach( Cosmic.deleteObject(config, { slug, : config.bucket.write_key }, (err, response) => { (err) { .log(err) .setState({ : , : }) } { .setState({ : , : }) Cosmic.getObjectType( .state.config, { : }, (err, response) => err ? .handleFetchError(err) : .handleFetch(response) )} } ) ) .setState({ : [], : }) } const this return => row const this const 4 'M-D-YYYY' 'YYYY-DD-MM' const 5 'h:mm a' 9 0 0 'hours' 1 return => appointment => slug write_key if console this snackbarDisabled false snackbarMessage 'Failed to delete appointments' else this snackbarMessage 'Loading...' snackbarDisabled false this type_slug 'appointments' this this this selectedRows deleteButtonDisabled true 10. Putting It All Together At this point, including all necessary imports, our completed extension looks like this: { Component } from injectTapEventPlugin from axios from async from _ from moment from Cosmic from AppBar from FlatButton from RaisedButton from SnackBar from DropDownMenu from MenuItem from DatePicker from { Toolbar, ToolbarGroup } from { Table, TableBody, TableHeader, TableHeaderColumn, TableRow, TableRowColumn, } from ; injectTapEventPlugin() export { (props) { (props) .state = { config: props.config, snackbarDisabled: , snackbarMessage: , toolbarDropdownValue: , appointments: {}, filteredAppointments: {}, datePickerDisabled: , selectedRows: [], deleteButtonDisabled: , allRowsSelected: } .handleFetchError = .handleFetchError.bind( ) .handleFetch = .handleFetch.bind( ) .handleRowSelection = .handleRowSelection.bind( ) .handleToolbarDropdownChange = .handleToolbarDropdownChange.bind( ) .handleDelete = .handleDelete.bind( ) .checkDisableDate = .checkDisableDate.bind( ) .setTableChildren = .setTableChildren.bind( ) } handleFetchError(err) { console.log(err) .setState({ snackbarMessage: }) } handleFetch(response) { appointments = response.objects.all ? response.objects.all.reduce((currentAppointments, appointment) => { date = appointment.metadata.date (!currentAppointments[date]) currentAppointments[date] = [] 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) currentAppointments }, {}) : {} .setState({ appointments, snackbarDisabled: , tableChildren: .setTableChildren([], appointments) }) } handleToolbarDropdownChange( ) { ? .setState({ filteredAppointments: {}, datePickerDisabled: , toolbarDropdownValue: }) : .setState({ toolbarDropdownValue: , datePickerDisabled: }) } handleRowSelection(rowsToSelect) { allRows = [...Array( .state.tableChildren.length).keys()] allRowsSelected = rowsToSelect === selectedRows = Array.isArray(rowsToSelect) ? rowsToSelect : allRowsSelected ? allRows : [] appointments = _.isEmpty( .state.filteredAppointments) ? .state.appointments : .state.filteredAppointments deleteButtonDisabled = selectedRows.length == tableChildren = allRowsSelected ? .setTableChildren([], appointments) : .setTableChildren(selectedRows, appointments) .setState({ selectedRows, deleteButtonDisabled, tableChildren }) } handleDelete(selectedRows) { { config } = .state selectedRows.map(row => { { tableChildren, appointments } = .state date = moment(tableChildren[row].props.children[ ].props.children, ).format( ) slot = moment(tableChildren[row].props.children[ ].props.children, ).diff(moment().hours( ).minutes( ).seconds( ), ) + _.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) => { (err) { console.log(err) .setState({ snackbarDisabled: , snackbarMessage: }) } { .setState({ snackbarMessage: , snackbarDisabled: }) Cosmic.getObjectType( .state.config, { type_slug: }, (err, response) => err ? .handleFetchError(err) : .handleFetch(response) )} } ) ) .setState({ selectedRows: [], deleteButtonDisabled: }) } checkDisableDate(day) { ! .state.appointments[moment(day).format( )] } filterAppointments(date) { dateString = moment(date).format( ) filteredAppointments = {} filteredAppointments[dateString] = .state.appointments[dateString] .setState({ filteredAppointments, tableChildren: .setTableChildren([], filteredAppointments) }) } setTableChildren(selectedRows = .state.selectedRows, appointments = .state.appointments) { renderAppointment = (date, appointment, index) => { { name, email, phone, slot } = appointment rowSelected = selectedRows.includes(index) <TableRow key={index} selected={rowSelected}> <TableRowColumn>{index}</TableRowColumn> <TableRowColumn>{name}</TableRowColumn> <TableRowColumn>{email}</TableRowColumn> <TableRowColumn>{phone}</TableRowColumn> <TableRowColumn>{moment(date, ).format( )}</TableRowColumn> <TableRowColumn>{moment().hour( ).minute( ).add(slot, ).format( )}</TableRowColumn> </TableRow> } appointmentsAreFiltered = !_.isEmpty( .state.filteredAppointments) schedule = appointmentsAreFiltered ? .state.filteredAppointments : appointments els = [] let counter = 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, ).isBefore(moment(b, ))) .forEach((date, index) => { schedule[date].forEach(appointment => { els.push(renderAppointment(date, appointment, counter)) counter++ }) }) els } componentWillMount() { Cosmic.getObjectType( .state.config, { type_slug: }, (err, response) => err ? .handleFetchError(err) : .handleFetch(response) ) } render() { { snackbarDisabled, appointments, datePickerDisabled, deleteButtonDisabled, ... } = .state ( <div style={{ fontFamily: }}> <AppBar showMenuIconButton={ } title= /> <SnackBar message={ .snackbarMessage} ={!snackbarDisabled} /> <Toolbar> <ToolbarGroup firstChild={ }> <DropDownMenu value={ .toolbarDropdownValue} onChange={(evt, key, ) => .handleToolbarDropdownChange( )}> <MenuItem value={ } primaryText= /> <MenuItem value={ } primaryText= /> </DropDownMenu> <DatePicker hintText= autoOk={ } disabled={datePickerDisabled} name= onChange={(n, date) => .filterAppointments(date)} shouldDisableDate={(day) => .checkDisableDate(day)} /> </ToolbarGroup> <ToolbarGroup lastChild={ }> <RaisedButton primary={ } onClick={() => .handleDelete( .selectedRows)} disabled={deleteButtonDisabled} label={`Delete Selected ${ .selectedRows.length ? + .selectedRows.length + : }`} /> </ToolbarGroup> </Toolbar> <Table onRowSelection={rowsToSelect => .handleRowSelection(rowsToSelect)} multiSelectable={ } > <TableHeader> <TableRow> <TableHeaderColumn>ID</TableHeaderColumn> <TableHeaderColumn>Name</TableHeaderColumn> <TableHeaderColumn>Email</TableHeaderColumn> <TableHeaderColumn>Phone</TableHeaderColumn> <TableHeaderColumn>Date</TableHeaderColumn> <TableHeaderColumn>Time</TableHeaderColumn> </TableRow> </TableHeader> <TableBody children={ .tableChildren} allRowsSelected={ .allRowsSelected}> </TableBody> </Table> </div> ) } } // ./src/Components/App.js import 'react' import 'react-tap-event-plugin' import 'axios' import 'async' import 'lodash' import 'moment' import 'cosmicjs' import 'material-ui/AppBar' import 'material-ui/FlatButton' import 'material-ui/RaisedButton' import 'material-ui/SnackBar' import 'material-ui/DropDownMenu' import 'material-ui/MenuItem' import 'material-ui/DatePicker' import 'material-ui/Toolbar' import 'material-ui/Table' default class App extends Component constructor super this false 'Loading...' 1 true true false this this this this this this this this this this this this this this this this this this this this this this 'Error loading data' const const if const return this true this val //0: filter by date, 1: list all val this true 1 this 0 false const this const 'all' const const this this this const 0 const this this this const this return const this const 4 'M-D-YYYY' 'YYYY-DD-MM' const 5 'h:mm a' 9 0 0 'hours' 1 return if this false 'Failed to delete appointments' else this 'Loading...' false this 'appointments' this this this true return this 'YYYY-DD-MM' const 'YYYY-DD-MM' const this this this this this const const const return 'YYYY-DD-MM' 'M[/]D[/]YYYY' 9 0 'hours' 'h:mm a' const this const this const 0 'YYYY-DD-MM' 'YYYY-MM-DD' return this 'appointments' this this const data this return 'Roboto' false "Appointment Manager" data open true data val this val 0 "Filter Appointments By Date" 1 "List All Appointments" "Select a date" true "date-select" this this true true this data data '(' data ')' '' this true data data Part 5: Conclusion Once you run in , create in to make Cosmic recognize it: webpack appointment-scheduler-extension extension.json dist // appointment-scheduler-extension/dist/extension.json { : , : , : } "title" "Appointment Manager" "font_awesome_class" "fa-calendar" "image_url" "" Then, compress , upload it to Cosmic, and we're ready to start managing appointments. dist 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