react调度时间原理_使用React,Twilio和Cosmic JS构建约会调度程序

react调度时间原理

TL; DR

您的时间很宝贵,但您不能浪费一秒钟。 人们需要看到您,因为工作需要完成并且需要进行协作。 除了让人们直接与您交流以安排他们对您的时间的使用(这只会浪费更多时间)之外,我们将使用Cosmic JS构建约会计划程序。 这样,需要与您交谈的人只需要一次。

Cosmic JS是API优先的CMS,这意味着它与语言无关,与数据库无关,以及几乎所有其他方面无关。 这对于像这样的小型项目非常有用,因为我们将来可以使用任何语言或框架快速扩展它,并且我们可以定义只需要我们复杂的数据结构。

我们的约会计划程序将使用户选择上午9点至下午5点之间的一天和一个小时的时段与我们会面。 然后,我们将把我们的应用程序与Twilio集成在一起,向他们发送确认消息,说明他们已经安排好约会。 最后,我们将构建一个Cosmic JS扩展,以便我们可以直接从Cosmic JS仪表板内部管理约会。

(我们的约会计划程序应用将是什么样子)

我们将在3个主要部分中完成我们的项目:

  • 借助Material UI组件库,在React和Webpack中构建前端
  • 将其连接到一个简单的Express后端,该后端将用于对Twilio进行API调用,并将我们的Appointment对象暴露给前端(本着将Cosmic存储桶密钥保留在前端代码之外的精神)
  • 再次使用Material UI库在React中构建扩展

但是,在此之前,我们需要准备好Cosmic存储桶以存储和服务数据。

第0部分:设置Cosmic JS

我们将使用两种类型的对象来存储数据:一种用于约会,另一种用于站点配置。 在Cosmic JS中,首先使用指定的元字段创建Appointments对象类型。 然后,我们将创建一个没有默认元字段的Configs对象类型,以及一个我们将在其中定义特定于对象的元字段的Siteobject。

预约时间

在这里, emailphone将成为用户的元数据-我们将使用他们的名称作为约会对象的标题。 Date将持有预约日期YYYY-DD-MM格式和slot将是任命多少小时的路程是从上午9点。

配置 / 站点

我们正在使用Config对象定义有关我们希望能够即时更改的应用程序的详细信息,而不必进行重新部署。

有了简单的数据方案,我们就可以开始构建了。

第1部分:构建前端

1.样板设置

首先,我们将创建约会调度程序目录,运行yarn init(可随时使用npm),并按如下所示设置项目结构:

appointment-scheduler
|
| --dist
 | --src
 | .  | --Components
 | .  | .  | --App.js
 | .  | --index.html
 | .  | --index.js
 | --.babelrc
 | --.gitignore
 | --package.json
 | --webpack.config.js

然后,我们将创建HTML模板:

<!-- ./src/index.html -->
<!DOCTYPE html>
< html >
  < head >
    < meta charset = "utf-8" >
    < meta name = "viewport" content = "width=device-width, initial-scale=1, maximum-scale=1" >
    < link href = "https://fonts.googleapis.com/css?family=Roboto" rel = "stylesheet" >
    < title > Appointment Scheduler </ title >
  </ head >
  < body >
    < div id = "root" > </ div >
  </ body >
</ html >

接下来,我们将安装所需的软件包。 有趣的是,除了在ES6中开发React应用所需的任何标准软件包之外,我们还将使用:

  • async进行串联Ajax调用的简单方法
  • axios作为有用的ajax实用程序
  • babel-preset-stage-3使用const {property} =对象解构模式
  • material-ui用于一组方便的React构建的Material Design组件
  • moment解析次
  • normalize.css清除浏览器默认样式
  • react-tap-event-plugin - material-ui的必要伴侣

要安装我们需要的一切,请运行

yarnadd async axios babel-preset-stage -3 material-ui moment normalze.css react react-dom react-tap- event -plugin

为了我们的开发依赖性

yarn add  babel-core  babel-loader  babel-preset-env  babel-preset-react  css-loader eslint file-loader html-webpack-plugin path style-loader webpack

安装完所需的Babel软件包后,我们将告诉Babel在配置文件中使用它们,并告诉git忽略刚安装的所有内容:

  • // ./.babelrc {“预设”:[“ env”,“ react”,“ stage-3”]}
  • #./.gitignore node_modules

最后,我们将设置Webpack,以便在构建时一切就绪

const path = require (‘path’) const HtmlWebpackPlugin = require (‘html-webpack-plugin’) const webpack = require (‘webpack’) module .exports = { entry : ‘./src/index.js’, output : { path : path.resolve(‘dist’), filename : ‘bundle.js’, sourceMapFilename : ‘bundle.map.js’ }, devtool : ‘source-map’, devServer : { port : 8080 }, module : { rules : [{ test : /\.js$/ , use : { loader : ‘babel-loader’ }, exclude : path.resolve(‘node_modules’) }, { test : [ /\.scss$/ ,/\.css$/], loader : [‘style-loader’, ‘css-loader’, ‘sass-loader’] }, { test : /\.(png|jpg|gif|svg)$/ , use : [ { loader : ‘file-loader’ } ] }] }, plugins : [ new HtmlWebpackPlugin({ template : ‘./src/index.html’, filename : ‘index.html’, inject : true , xhtml : true }), new webpack.DefinePlugin({ PRODUCTION : process.env.NODE_ENV === ‘production’ }) ] }
const path = require (‘path’) 
const HtmlWebpackPlugin = require (‘html-webpack-plugin’) 
const webpack = require (‘webpack’) 

module .exports = { 
	entry : ‘./src/index.js’, 
	output : {
	 	path : path.resolve(‘dist’), filename : ‘bundle.js’, sourceMapFilename : ‘bundle.map.js’ 
	 }, 
	 devtool : ‘source-map’, 
	 devServer : { port : 8080 }, 
	 module : { rules : [{ 
		 test : /\.js$/ , use : { loader : ‘babel-loader’ }, 
		 exclude : path.resolve(‘node_modules’) 
		 }, { 
		 test : [ /\.scss$/ ,/\.css$/], loader : [‘style-loader’, ‘css-loader’, ‘sass-loader’] 
		 }, {
		  test : /\.(png|jpg|gif|svg)$/ , use : [ { loader : ‘file-loader’ } ] 
		 }] 
	}, 
	plugins : [ 
		new HtmlWebpackPlugin({ 
			template : ‘./src/index.html’, 
			filename : ‘index.html’, 
			inject : true , 
			xhtml : true 
		}), 
		new webpack.DefinePlugin({ 
			PRODUCTION : process.env.NODE_ENV === ‘production’ 
		}) 
	] 
}

我们已经配置的WebPack输出bundle.js ,它的源地图和index.html (根据模板src ),以dist建设。 您还可以选择在整个项目中使用SCSS,并可以在构建时通过window.PRODUCTION访问Node环境。

2.创建一个入口点

在继续之前,我们需要为我们的应用程序定义一个入口点。 在这里,我们将导入所有必要的全局库或包装器组件。 我们还将使用它作为React应用程序的渲染点。 这将是我们的src/index.js文件,看起来像这样:

// ./src/index.js
import React from 'react'
import ReactDom from 'react-dom'
import App from './Components/App'
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider'
import 'normalize.css'
require ( './scss/app.scss' )
window .React = React
ReactDom.render(
  < MuiThemeProvider >
     <App /> 
  </ MuiThemeProvider > ,
  document .getElementById( 'root' )
)
// MuiThemeProvider is a wrapper component for MaterialUI's components

3.算出骨架

为了使我们的应用正常运行,我们有很多事情要考虑,在开始构建应用之前,我们需要一些选择来决定我们的应用如何工作。 为了帮助我们思考,我们将构建一个基本框架,以显示其外观:

// ./src/Components/App.js
import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
injectTapEventPlugin()
export default class App extends Component  {
    constructor () {
    super ()
      this .state = {
            // initial state
        }
        
  //method bindings
    }
  
  //component methods?
  
  //lifecycle methods
componentWillMount() {
        //fetch data from cosmic, watch window width
    }
  componentWillUnmount() {
        //remove window width event listener
    }
  render() {
     //define variables
      return (
        < div >
          </ div >
        )
    }
}

首先,我们需要考虑应用程序的状态。 以下是一些注意事项:

  • 该应用程序从外部服务器加载数据,因此在发生这种情况时向我们展示用户的信息将非常有用
  • Material Design实现了抽屉式导航,因此我们需要跟踪打开的时间
  • 为了在提交之前确认用户的约会详细信息,我们将向他们显示确认模式,对于其他通知,我们将使用Material Design的快餐栏,该快餐栏在页面底部显示小通知。 我们需要跟踪这两者的打开状态。
  • 我们的约会安排过程将分三个步骤进行:选择日期,选择时间段以及填写个人信息。 我们需要跟踪用户所处的步骤,选择的日期和时间,他们的联系方式,并且还需要验证他们的电子邮件地址和电话号码。
  • 我们将加载配置数据和计划约会,这将受益于该状态的缓存。
  • 在我们的用户执行3个调度步骤的过程中,我们将显示一个友好的句子来跟踪其进度。 (例如:“在…下午3点安排1小时约会”)。 我们需要知道是否应该显示它。
  • 当用户选择分配槽时,他们将能够按AM / PM进行过滤,因此我们需要跟踪他们正在寻找的对象。
  • 最后,我们将为样式添加一些响应能力,并且需要跟踪屏幕宽度。

然后我们将其作为初始状态:

// ./src/Components/App.js
// ...
this .state = {
  loading : true ,
  navOpen : false ,
  confirmationModalOpen : false ,
  confirmationTextVisible : false ,
  stepIndex : 0 ,
  appointmentDateSelected : false ,
  appointmentMeridiem : 0 ,
  validEmail : false ,
  validPhone : false ,
  smallScreen : window .innerWidth < 768 ,
  confirmationSnackbarOpen : false
}

注意appointmentMeridiem01 ,这样0 => 'AM'1 => 'PM'

4.草拟功能

我们已经为我们的应用定义了初始状态,但是在我们使用Material组件构建视图之前,我们将发现对集思广益处理数据很有用。 我们的应用程序将归结为以下功能:

  • 如上一步所决定,我们的导航将在抽屉中,因此我们需要handleNavToggle()方法来显示/隐藏它
  • 完成上一个步骤后,这三个调度步骤将依次显示给用户,因此我们需要一个handleNextStep()方法来处理用户输入的流程
  • 我们将使用Material UI日期选择器设置约会日期,并且需要handleSetAppointmentDate()方法来处理该组件中的数据。 同样,我们需要一个handleSetAppointmentSlot()handleSetAppointmentMeridiem()方法。 我们不希望日期选择器显示不可用的日期(包括today ),因此我们需要将checkDisableDate()方法传递给它。
  • componentWillMount()生命周期方法中,我们将从后端获取数据,然后使用单独的handleFetch()方法处理该数据。 对于获取错误,我们需要一个handleFetchError()方法。
  • 提交约会数据后,我们将使用handleSubmit()将其发送到后端。 当用户填写联系信息时,我们将需要validateEmail()validatePhone()方法。
  • 表单上方的用户友好字符串将使用renderConfirmationString()在单独的方法中renderConfirmationString() 。 因此可用约会时间和分别使用renderAppointmentTimes()renderAppointmentConfirmation()的确认模式。
  • 最后,我们将使用方便的resize()方法来响应浏览器窗口宽度的变化

总而言之,包括未App.js方法,我们的App.js现在看起来像这样(包括方法绑定):

// ./src/Components/App.js
import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
injectTapEventPlugin()
export default class App extends Component  {
    constructor () {
    super ()
      this .state = {
          loading : true ,
          navOpen : false ,
          confirmationModalOpen : false ,
          confirmationTextVisible : false ,
          stepIndex : 0 ,
          appointmentDateSelected : false ,
          appointmentMeridiem : 0 ,
          validEmail : false ,
          validPhone : false ,
          smallScreen : window .innerWidth < 768 ,
          confirmationSnackbarOpen : false
        }
       
      //method bindings
      this .handleNavToggle = this .handleNavToggle.bind( this )
      this .handleNextStep = this .handleNextStep.bind( this )
      this .handleSetAppointmentDate = this .handleSetAppointmentDate.bind( this )
      this .handleSetAppointmentSlot = this .handleSetAppointmentSlot.bind( this )
      this .handleSetAppointmentMeridiem = this .handleSetAppointmentMeridiem.bind( this )
      this .handleSubmit = this .handleSubmit.bind( this )
      this .validateEmail = this .validateEmail.bind( this )
      this .validatePhone = this .validatePhone.bind( this )
      this .checkDisableDate = this .checkDisableDate.bind( this )
      this .renderAppointmentTimes = this .renderAppointmentTimes.bind( this )
      this .renderConfirmationString = this .renderConfirmationString.bind( this )
      this .renderAppointmentConfirmation = this .renderAppointmentConfirmation.bind( this )
      this .resize = this .resize.bind( this )
    }
  
  handleNavToggle() {
        
   }
  
  handleNextStep() {
        
    }
  
  handleSetAppointmentDate(date) {
        
    }
  
  handleSetAppointmentSlot(slot) {
        
    }
  
  handleSetAppointmentMeridiem(meridiem) {
        
    }
  
  handleFetch(response) {
        
    }
  
  handleFetchError(err) {
        
    }
  
  handleSubmit() {
        
    }
  
  validateEmail(email) {
        
    }
  
  validatePhone(phoneNumber) {
        
    }
  
  checkDisableDate(date) {
        
    }
  
  renderConfirmationString() {
        
    }
  
  renderAppointmentTimes() {
        
    }
  
  renderAppointmentConfirmation() {
        
    }
  
  resize() {
        
    }
  
  //lifecycle methods
componentWillMount() {
        //fetch data from cosmic, watch window width
    }
  componentWillUnmount() {
        //remove window width event listener
    }
  render() {
     //define variables
      return (
        < div >
          </ div >
        )
    }
}

5.建立视图

有了我们的应用程序如何运行的基本概念,我们就可以开始构建其UI。 除了几个包装器和一些自定义样式外,我们的大多数应用程序都将使用预包装的Material UI组件构建。

为了我们,需要:

  • 一个AppBar充当主要工具栏
  • Drawer ,从AppBar的主按钮打开,并作为Material Design之后的应用程序导航。
  • DrawerMenuItem用于显示链接
  • Card作为主要内容容器
  • Stepper将调度过程分为3个谨慎的步骤。 活动步骤将被扩展,而其他步骤将被折叠。 如果state.loading为true,则将禁用state.loading ,并且只要用户尚未填写上一步,则将禁用后两步。
  • 嵌套在Stepper ,三个Steps包含StepButtonStepContent组件。
  • Step ,我们将使用DatePicker让用户选择一个约会日期。 根据checkDisableDate()的返回值,将禁用不可用的日期。 日期的选择将通过handleSetAppointmentDate()处理
  • Step ,我们希望用户能够从可用的时段中为其选定日期选择一个时段。 我们还希望他们能够根据AM / PM过滤时间。 我们将使用SelectField作为过滤器,并使用RadioButtonGroup来保存时隙按钮。 我们需要额外的逻辑来渲染单选按钮,因此将在renderAppointmentTimes()方法中实现。 这将返回一组RadioButton
  • 最后Step将要求用户使用TextField组件输入其姓名,电子邮件地址和电话号码。 将使用RaisedButton的提交按钮打开确认Dialog 。 用户输入的电话号码和电子邮件地址将分别通过validatePhone()validateEmail()
  • 最后,我们将在页面底部提供一个方便的SnackBar以显示有关加载状态和提交的通知。 总而言之,写出render()方法后,我们的应用程序将如下所示:
  • // ./src/Components/App.js
    // .. previous imports
    import AppBar from 'material-ui/AppBar'
    import Drawer from 'material-ui/Drawer'
    import Dialog from 'material-ui/Dialog'
    import Divider from 'material-ui/Divider'
    import MenuItem from 'material-ui/MenuItem'
    import Card from 'material-ui/Card'
    import DatePicker from 'material-ui/DatePicker'
    import TimePicker from 'material-ui/TimePicker'
    import TextField from 'material-ui/TextField'
    import SelectField from 'material-ui/SelectField'
    import SnackBar from 'material-ui/Snackbar'
    import {
      Step,
      Stepper,
      StepLabel,
      StepContent,
      StepButton
    } from 'material-ui/stepper'
    import {
      RadioButton,
      RadioButtonGroup
    } from 'material-ui/RadioButton'
    import RaisedButton from 'material-ui/RaisedButton' ;
    import FlatButton from 'material-ui/FlatButton'
    import logo from './../../dist/assets/logo.svg'
    export default class App extends Component  {
        // ... component methods, lifecycle methods
      render() {
        const { stepIndex, loading, navOpen, smallScreen, confirmationModalOpen, confirmationSnackbarOpen, ...data } = this .state
        const contactFormFilled = data.firstName && data.lastName && data.phone && data.email && data.validPhone && data.validEmail
        const modalActions = [
          < FlatButton
            label = "Cancel"
            primary = {false}
            onClick = {() => this.setState({ confirmationModalOpen : false})} />,
           <FlatButton
            label="Confirm"
            primary={true}
            onClick={() => this.handleSubmit()} />
        ]
        return (
          <div>
            <AppBar
              title={data.siteTitle}
              onLeftIconButtonTouchTap={() => this.handleNavToggle() }/>
            <Drawer
              docked={false}
              width={300}
              open={navOpen}
              onRequestChange={(navOpen) => this.setState({navOpen})} >
              <img src={logo}
                   style={{
                     height: 70,
                     marginTop: 50,
                     marginBottom: 30,
                     marginLeft: '50%',
                     transform: 'translateX(-50%)'
                   }}/>
              <a style={{textDecoration: 'none'}} href={this.state.homePageUrl}><MenuItem>Home</MenuItem></a>
              <a style={{textDecoration: 'none'}} href={this.state.aboutPageUrl}><MenuItem>About</MenuItem></a>
              <a style={{textDecoration: 'none'}} href={this.state.contactPageUrl}><MenuItem>Contact</MenuItem></a>
              <MenuItem disabled={true}
                        style={{
                          marginLeft: '50%',
                          transform: 'translate(-50%)'
                        }}>
                {"© Copyright " + moment().format('YYYY')}</MenuItem>
            </Drawer>
            <section style={{
                maxWidth: !smallScreen ? '80%' : '100%',
                margin: 'auto',
                marginTop: !smallScreen ? 20 : 0,
              }}>
              {this.renderConfirmationString()}
              <Card style={{
                  padding: '10px 10px 25px 10px',
                  height: smallScreen ? '100vh' : null
                }}>
                <Stepper
                  activeStep={stepIndex}
                  linear={false}
                  orientation="vertical">
                  <Step disabled={loading}>
                    <StepButton onClick={() => this.setState({ stepIndex: 0 })}>
                      Choose an available day for your appointment
                    </StepButton>
                    <StepContent>
                      <DatePicker
                          style={{
                            marginTop: 10,
                            marginLeft: 10
                          }}
                          value={data.appointmentDate}
                          hintText="Select a date"
                          mode={smallScreen ? 'portrait' : 'landscape'}
                          onChange={(n, date) => this.handleSetAppointmentDate(date)}
                          shouldDisableDate={day => this.checkDisableDate(day)}
                           />
                      </StepContent>
                  </Step>
                  <Step disabled={ !data.appointmentDate }>
                    <StepButton onClick={() => this.setState({ stepIndex: 1 })}>
                      Choose an available time for your appointment
                    </StepButton>
                    <StepContent>
                      <SelectField
                        floatingLabelText="AM or PM"
                        value={data.appointmentMeridiem}
                        onChange={(evt, key, payload) => this.handleSetAppointmentMeridiem(payload)}
                        selectionRenderer={value => value ? 'PM' : 'AM'}>
                        <MenuItem value={0}>AM</MenuItem>
                        <MenuItem value={1}>PM</MenuItem>
                      </SelectField>
                      <RadioButtonGroup
                        style={{ marginTop: 15,
                                 marginLeft: 15
                               }}
                        name="appointmentTimes"
                        defaultSelected={data.appointmentSlot}
                        onChange={(evt, val) => this.handleSetAppointmentSlot(val)}>
                        {this.renderAppointmentTimes()}
                      </RadioButtonGroup>
                    </StepContent>
                  </Step>
                  <Step disabled={ !Number.isInteger(this.state.appointmentSlot) }>
                    <StepButton onClick={() => this.setState({ stepIndex: 2 })}>
                      Share your contact information with us and we'll send you a reminder
                    </StepButton>
                    <StepContent>
                      <section>
                        <TextField
                          style={{ display: 'block' }}
                          name="first_name"
                          hintText="First Name"
                          floatingLabelText="First Name"
                          onChange={(evt, newValue) => this.setState({ firstName: newValue })}/>
                        <TextField
                          style={{ display: 'block' }}
                          name="last_name"
                          hintText="Last Name"
                          floatingLabelText="Last Name"
                          onChange={(evt, newValue) => this.setState({ lastName: newValue })}/>
                        <TextField
                          style={{ display: 'block' }}
                          name="email"
                          hintText="name@mail.com"
                          floatingLabelText="Email"
                          errorText={data.validEmail ? null : 'Enter a valid email address'}
                          onChange={(evt, newValue) => this.validateEmail(newValue)}/>
                        <TextField
                          style={{ display: 'block' }}
                          name="phone"
                          hintText="(888) 888-8888"
                          floatingLabelText="Phone"
                          errorText={data.validPhone ? null: 'Enter a valid phone number'}
                          onChange={(evt, newValue) => this.validatePhone(newValue)} />
                        <RaisedButton
                          style={{ display: 'block' }}
                          label={contactFormFilled ? 'Schedule' : 'Fill out your information to schedule'}
                          labelPosition="before"
                          primary={true}
                          fullWidth={true}
                          onClick={() => this.setState({ confirmationModalOpen: !this.state.confirmationModalOpen })}
                          disabled={!contactFormFilled || data.processed }
                          style={{ marginTop: 20, maxWidth: 100}} />
                      </section>
                    </StepContent>
                  </Step>
                </Stepper>
              </Card>
              <Dialog
                modal={true}
                open={confirmationModalOpen}
                actions={modalActions}
                title="Confirm your appointment">
                {this.renderAppointmentConfirmation()}
              </Dialog>
              <SnackBar
                open={confirmationSnackbarOpen || loading}
                message={loading ? 'Loading... ' : data.confirmationSnackbarMessage || ''}
                autoHideDuration={10000}
                onRequestClose={() => this.setState({ confirmationSnackbarOpen: false })} />
            </section>
          </div>
        )
      }
    }

在继续之前,请注意,因为它们需要渲染一些额外的逻辑,所以我们将在自己的方法中将时隙和确认字符串的单选按钮交给他们。

使用适当的视图组件后,我们的最后一步将是编写我们映射出的所有功能。

6.组件生命周期方法

componentWillMount()

添加功能的第一步将是写出componentWillMount()方法。 在componentWillMount()我们将使用axios从后端获取我们的配置和约会数据。 同样,我们将后端用作中间人,因此我们可以有选择地向前端公开数据,并省略诸如用户联系信息之类的内容。

// ./src/Components/App.js
// previous imports
import async from 'async'
import axios from 'axios'
export default class App extends Component  {
constructor () {}
  
  componentWillMount() {
    async .series({
        configs(callback) {
          axios.get(HOST + 'api/config' ).then( res =>
            callback( null , res.data.data)
          )
        },
        appointments(callback) {
          axios.get(HOST + 'api/appointments' ).then( res => {
            callback( null , res.data.data)
          })
       }
      }, (err,response) => {
        err ? this .handleFetchError(err) : this .handleFetch(response)
    })
    addEventListener( 'resize' , this .resize)
    }
  
 // rest...
}

我们使用async axios进行axios调用,并命名它们,以便我们可以在handleFetch()它们作为response.configsresponse.appointments进行handleFetch() 。 我们还使用componentWillMount通过resize()开始跟踪窗口宽度。

componentWillUnmount()

练习良好的形式,我们将在componentWillUnmount()删除事件侦听器。

// ./src/Components/App.js
// previous imports
import async from 'async'
import axios from 'axios'
export default class App extends Component  {
constructor () {}
  
  componentWillUnmount() {
removeEventListener( 'resize' , this .resize)
    }
  
 // rest...
}

7.处理数据

提取了所需的数据后,我们将使用handleFetch()成功处理获取,并使用handleFetchError()错误。 在handleFetch()我们将构建一个约会计划以存储在状态中,例如schedule = { appointmentDate: [slots] } 。 我们还使用此方法在状态下存储应用程序的配置数据。

handleFetch()

handleFetch(response) {const { configs, appointments } = response
    const initSchedule = {}
    const today = moment().startOf( 'day' )
    initSchedule[today.format( 'YYYY-DD-MM' )] = true
    const schedule = !appointments.length ? initSchedule : appointments.reduce( ( currentSchedule, appointment ) => {
      const { date, slot } = appointment
      const dateString = moment(date, 'YYYY-DD-MM' ).format( 'YYYY-DD-MM' )
      !currentSchedule[date] ? currentSchedule[dateString] = Array ( 8 ).fill( false ) : null
      Array .isArray(currentSchedule[dateString]) ?
        currentSchedule[dateString][slot] = true : null
      return currentSchedule
    }, initSchedule)
    for ( let day in schedule) {
      let slots = schedule[day]
      slots.length ? (slots.every( slot => slot === true )) ? schedule[day] = true : null : null
    }
    this .setState({
      schedule,
      siteTitle : configs.site_title,
      aboutPageUrl : configs.about_page_url,
      contactPageUrl : configs.contact_page_url,
      homePageUrl : configs.home_page_url,
      loading : false
    })
  }

handleFetchError()

为了处理错误,我们仅在SnackBar向用户显示错误消息。

handleFetchError(err) {console .log( 'Error fetching data:' + err)
    this .setState({ confirmationSnackbarMessage : 'Error fetching data' , confirmationSnackbarOpen : true })
}

8.处理UI更改

每当用户打开抽屉,移至另一步骤或浏览器宽度发生变化时,我们都需要管理状态。 首先,我们将处理抽屉开关。

handleNavToggle() {return this .setState({ navOpen : ! this .state.navOpen })
}

然后,只要用户不在最后一步,我们就将逐步增加步长。

handleNextStep() {const { stepIndex } = this .state
  return (stepIndex < 3 ) ? this .setState({ stepIndex : stepIndex + 1 }) : null
}

最后,如果窗口宽度小于768px,我们将简单地更改大小调整状态。

resize() {this .setState({ smallScreen : window .innerWidth < 768 })
}

9.处理设置约会数据

当用户在步骤1和步骤2中选择约会选项时,我们需要三个简单的设置器来更改状态以反映这些选择。

handleSetAppointmentDate(date) {this .handleNextStep()
  this .setState({ appointmentDate : date, confirmationTextVisible : true })
}
handleSetAppointmentSlot(slot) {
  this .handleNextStep()
  this .setState({ appointmentSlot : slot })
}
handleSetAppointmentMeridiem(meridiem) {
  this .setState({ appointmentMeridiem : meridiem})
}

10.处理验证

我们需要验证用户输入的电子邮件地址,电话号码,并且需要向DatePicker组件提供一个功能,以检查应禁用哪些日期。 尽管很幼稚,但为了简单起见,我们将使用正则表达式来检查输入。

validateEmail(email) {const regex = /^(([^<>()\[\]\.,;:\s@\"]+(\.[^<>()\[\]\.,;:\s@\"]+)*)|(\".+\"))@(([^<>()[\]\.,;:\s@\"]+\.)+[^<>()[\]\.,;:\s@\"]{2,})$/i
  return regex.test(email) ? this .setState({ email : email, validEmail : true }) : this .setState({ validEmail : false })
}
validatePhone(phoneNumber) {
  const regex = /^(1\s|1|)?((\(\d{3}\))|\d{3})(\-|\s)?(\d{3})(\-|\s)?(\d{4})$/
  return regex.test(phoneNumber) ? this .setState({ phone : phoneNumber, validPhone : true }) : this .setState({ validPhone : false })
}

为了检查是否应禁用日期,我们需要检查DatePicker传递的日期是否处于state.schedule或今天。

checkDisableDate(day) {const dateString = moment(day).format( 'YYYY-DD-MM' )
    return this .state.schedule[dateString] === true || moment(day).startOf( 'day' ).diff(moment().startOf( 'day' )) < 0
  }

11.构建确认字符串和时隙单选按钮的渲染方法

在我们的render()生命周期方法中,我们提取了用于在Card上方显示动态确认字符串,将在确认模式中显示的确认详细信息以及用于选择时隙的单选按钮的逻辑。

从确认字符串开始,我们仅在输入时显示与输入数据相对应的部分。

renderConfirmationString() {
  const spanStyle = {color: '#00bcd4'}
  return this. state .confirmationTextVisible ? <h2 style={{ textAlign: this.state.smallScreen ? 'center' : 'left', color: '#bdbdbd', lineHeight: 1.5, padding: '0 10px', fontFamily: 'Roboto'}}>
    { <span>
     Scheduling a
     <span style={spanStyle}> 1 hour </span>
appointment {this. state .appointmentDate && <span>
  on <span style={spanStyle}> {moment(this. state .appointmentDate).format('dddd[,] MMMM Do')}</span>
             </span>} {Number.isInteger(this. state .appointmentSlot) && <span> at <span style={spanStyle}> {moment().hour( 9 ).minute( 0 ).add(this. state .appointmentSlot, 'hours').format('h:mm a')}</span></span>}
             </span>}
             </h2> : null
}

然后,类似地,我们将让用户在确认提交之前验证其数据。

renderAppointmentConfirmation() {
  const spanStyle = { color: '#00bcd4' }
  return <section>
    <p> Name: <span style={spanStyle}> {this. state .firstName} {this. state .lastName}</span></p>
      <p> Number: <span style={spanStyle}> {this. state .phone}</span></p>
        <p> Email: <span style={spanStyle}> {this. state .email}</span></p>
          <p> Appointment: <span style={spanStyle}> {moment(this. state .appointmentDate).format('dddd[,] MMMM Do[,] YYYY')}</span> at <span style={spanStyle}> {moment().hour( 9 ).minute( 0 ).add(this. state .appointmentSlot, 'hours').format('h:mm a')}</span></p>
  </section>
}

最后,我们将编写用于渲染约会槽单选按钮的方法。 为此,我们首先必须根据可用性以及是否选择AM或PM来过滤插槽。 两者都是简单的检查; 对于第一个,我们查看它是否存在于state.schedule ,对于第二个,我们使用moment().format('a')检查该子午线部分。 要以12小时格式计算时间字符串,我们以小时为单位将广告位添加到9AM。

renderAppointmentTimes() {if (! this .state.loading) {
    const slots = [...Array( 8 ).keys()]
    return slots.map(slot => {
      const appointmentDateString = moment( this .state.appointmentDate).format( 'YYYY-DD-MM' )
      const t1 = moment().hour( 9 ).minute( 0 ).add(slot, 'hours' )
      const t2 = moment().hour( 9 ).minute( 0 ).add(slot + 1 , 'hours' )
      const scheduleDisabled = this .state.schedule[appointmentDateString] ? this .state.schedule[moment( this .state.appointmentDate).format( 'YYYY-DD-MM' )][slot] : false
      const meridiemDisabled = this .state.appointmentMeridiem ? t1.format( 'a' ) === 'am' : t1.format( 'a' ) === 'pm'
      return <RadioButton
      label={t1.format( 'h:mm a' ) + ' - ' + t2.format( 'h:mm a' )}
      key={slot}
      value={slot}
      style={{marginBottom: 15 , display: meridiemDisabled ? 'none' : 'inherit' }}
                     disabled={scheduleDisabled || meridiemDisabled}/>
                     })
  } else {
    return null
  }
}

13.处理表格提交

向用户显示确认模式后,在最终提交后,我们将使用axios POST将数据发送到我们的后端。 我们会通知他们成功或失败。

handleSubmit() {const appointment = {
      date : moment( this .state.appointmentDate).format( 'YYYY-DD-MM' ),
      slot : this .state.appointmentSlot,
      name : this .state.firstName + ' ' + this .state.lastName,
      email : this .state.email,
      phone : this .state.phone
    }
    axios.post(HOST + 'api/appointments' , )
    axios.post(HOST + 'api/appointments' , appointment)
    .then( response => this .setState({ confirmationSnackbarMessage : "Appointment succesfully added!" , confirmationSnackbarOpen : true , processed : true }))
    .catch( err => {
      console .log(err)
      return this .setState({ confirmationSnackbarMessage : "Appointment failed to save." , confirmationSnackbarOpen : true })
    })
  }

14.结论:一起看

在继续构建后端之前,这是我们的最终产品。

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

第2部分:构建后端

1.安装和目录结构

我们的后端将很简单。 它要做的只是充当我们的前端和Cosmic之间的中介,并处理与Twilio的接口。

首先,我们准备好目录结构。

AppointmentScheduler|
|--public
|--app.js
|--.gitignore
|--package.json

public将是我们从中为其构建的前端提供服务的地方, .gitignore将隐藏node_modules

#  .gitignore
node_modules

我们将使用以下软件包:

  • 用于将约会对象推送到Cosmic的axios
  • 用于服务器软件的body-parsercorshttpmorganpathexpress
  • cosmicjs官方客户端,用于从Cosmic获取对象
  • twilio官方客户端,用于发送确认文本
  • moment解析次

运行yarn init ,然后使用启动脚本编辑package.json ,以便我们可以在Cosmic上进行部署。

{
    // etc..."scripts" : {
        "start" : "node app.js"
    }
}

然后,在我们开始工作之前:

yarnadd axios body- parser cors cosmicjs express express- session http moment morgan path twilio

2.概述后端的结构

就配置和中间件而言,我们的Express应用程序将是相当基本的。 对于Appointment提交,我们将在/api/appointments处理发帖请求。 我们将分别从/api/config/api/appointments提供站点配置和约会。 最后,由于我们的前端是SPA,因此我们将从/提供服务index.html ,并在那里重定向所有其他请求。

在深入探讨逻辑之前,我们的服务器将开始如下所示:

const express = require ( 'express' )
const path = require ( 'path' )
const morgan = require ( 'morgan' )
const bodyParser = require ( 'body-parser' )
const cors = require ( 'cors' )
const config = require ( './config' )
const http = require ( 'http' )
const Cosmic = require ( 'cosmicjs' )
const twilio = require ( 'twilio' )
const moment = require ( 'moment' )
const axios = require ( 'axios' )
const config = {
  bucket : {
    slug : process.env.COSMIC_BUCKET,
    read_key : process.env.COSMIC_READ_KEY,
    write_key : process.env.COSMIC_WRITE_KEY
  },
  twilio : {
    auth : process.env.TWILIO_AUTH,
    sid : process.env.TWILIO_SID,
    number : process.env.TWILIO_NUMBER
  }
}
const app = express()
const env = process.env.NODE_ENV || 'development'
const twilioSid = config.twilio.sid
const twilioAuth = config.twilio.auth
const twilioClient = twilio(twilioSid, twilioAuth)
const twilioNumber = config.twilio.number
app.set( 'trust proxy' , 1 )
app.use(session({
  secret : 'sjcimsoc' ,
  resave : false ,
  saveUninitialized : true ,
  cookie : { secure : false }
}))
app.use(cors())
app.use(morgan( 'dev' ))
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended : true }))
app.use(express.static(path.join(__dirname, 'public' )))
app.set( 'port' , process.env.PORT || 3000 )
app.post( '/api/appointments' , (req, res) => {
    //handle posting new appointments to Cosmic
  //and sending a confirmation text with Twilio
})
app.get( '/api/config' , (req, res) => {
    //fetch configs from Cosmic, expose to frontend
})
app.get( '/api/appointments' , (req, res) => {
    //fetch appointments from Cosmic, expose to frontend without personal data
})
app.get( '/' , (req, res) => {
    res.send( 'index.html' )
})
app.get( '*' , (req, res) => {
    res.redirect( '/' )
})
http.createServer(app).listen(app.get( 'port' ), () =>
  console .log( 'Server running at: ' + app.get( 'port' ))
)

注意:我们将在Cosmic部署时提供所有process.env变量。 宇宙专用变量是自动提供的。

3.处理过帐请求

这里需要发生两件事。 我们将使用官方的Twilio客户端向用户发送文本,并使用axios向Cosmic JS API发出POST请求。 在完成这两项操作之前,我们将剥去用户输入的任何非数字电话号码,并从选定的时段计算时间。

我们有:

app.post('/api/appointments' , (req, res) => {
  const appointment = req.body
  appointment.phone = appointment.phone.replace( /\D/g , '' )
  const date = moment(appointment.date, 'YYYY-DD-MM' ).startOf( 'day' )
  const time = date.hour( 9 ).add(appointment.slot, 'hours' )
  const smsBody = ` ${appointment.name} , this message is to confirm your appointment at ${time.format( 'h:mm a' )} on ${date.format( 'dddd MMMM Do[,] YYYY' )} .`
  //send confirmation message to user
  twilioClient.messages.create({
    to : '+1' + appointment.phone,
    from : twilioNumber,
    body : smsBody
  }, (err, message) => console .log(message, err))
  //push to cosmic
  const cosmicObject = {
    "title" : appointment.name,
    "type_slug" : "appointments" ,
    "write_key" : config.bucket.write_key,
    "metafields" : [
      {
        "key" : "date" ,
        "type" : "text" ,
        "value" : date.format( 'YYYY-DD-MM' )
      },
      {
        "key" : "slot" ,
        "type" : "text" ,
        "value" : appointment.slot
      },
      {
        "key" : "email" ,
        "type" : "text" ,
        "value" : appointment.email
      },{
        "key" : "phone" ,
        "type" : "text" ,
        "value" : appointment.phone //which is now stripped of all non-digits
      }
    ]
  }
  axios.post( `https://api.cosmicjs.com/v1/ ${config.bucket.slug} /add-object` , cosmicObject)
  .then( response => res.json({ data : 'success' })).catch( err => res.json({ data : 'error ' }))
})

4.公开站点配置

我们将简单地使用cosmicjs来获取前端所需的site-config对象,以在导航中显示链接。

app.get('/api/config' , (req,res) => {
  Cosmic.getObject(config, { slug : 'site-config' }, (err, response) => {
    const data = response.object.metadata
    err ? res.status( 500 ).json({ data : 'error' }) : res.json({ data })
  })
})

5.公开任命

通过后端API公开站点配置绝对是多余的,而使用Appointment对象来实现站点配置绝对重要。 首先,我们可以方便地重组数据以仅公开我们需要的数据,但是其次,更重要的是,我们不会公开公开用户的个人信息。 我们将使用cosmicjs来获取所有的Appointment对象,但只公开{ date, slot }形式的对象数组。

app.get('/api/appointments' , (req, res) => {
  Cosmic.getObjectType(config, { type_slug : 'appointments' }, (err, response) => {
    const appointments = response.objects.all ? response.objects.all.map( appointment => {
      return {
        date : appointment.metadata.date,
        slot : appointment.metadata.slot
      }
    }) : {}
    res.json({ data : appointments })
  })
})

6.成品

几分钟之内,这完全归功于Express的简单性,CosmicJs的官方客户和Twilio的官方客户,我们的后端可以完成我们想要做的一切,仅此而已。 纯禅。

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

第3部分:构建和部署

在构建扩展来管理约会之前,我们将捆绑前端并将应用程序部署到Cosmic,这样我们甚至可以管理一些约会。

在前端目录appointment-scheduler ,运行webpack将其构建到dist 。 然后将dist的内容移动到后端的公用文件夹AppointmentScheduler/public 。 该index.html是建立的WebPack然后将是index.html ,我们从服务/

AppointmentScheduler ,将应用程序提交到新的Github存储库。 然后,创建一个试用版Twilio帐户,并在Cosmic JS仪表板中,从deploy菜单中添加以下env变量。

  • TWILIO_AUTH您的Twilio身份验证密钥
  • TWILIO_SID您的Twilio TWILIO_SID
  • TWILIO_NUMBER您与Twilio试用版关联的电话号码。

现在继续部署并添加一些示例约会,我们可以使用这些约会来测试我们的扩展。

第4部分。构建扩展

通过Cosmic JS,您可以上传SPA,可用于在Cosmic JS仪表板中访问和操作存储桶的数据。 这些称为扩展,我们将构建一个扩展来查看所有预定约会的表格,并为我们提供一种简单的删除约会的方式。

与前端一样,我们将使用带有材质UI的React,此处的步骤与第1部分类似。

1.样板设置

首先,建立我们的appointment-scheduler-extension目录,运行yarn init ,并创建以下项目结构。

appointment-scheduler-extension
|
| --dist
 | --src
 | .  | --Components
 | .  | .  | --App.js
 | .  | --index.html
 | .  | --index.js
 | --.babelrc
 | --.gitignore
 | --package.json
 | --webpack.config.js

使用与前端相同的index.html模板。

<!-- ./src/index.html -->
<!DOCTYPE html>
< html >
  < head >
    < meta charset = "utf-8" >
    < meta name = "viewport" content = "width=device-width, initial-scale=1, maximum-scale=1" >
    < link href = "https://fonts.googleapis.com/css?family=Roboto" rel = "stylesheet" >
    < title > Appointment Scheduler </ title >
  </ head >
  < body >
    < div id = "root" > </ div >
  </ body >
</ html >
  1. 我们将几乎使用所有与前端相同的软件包。 有关安装的信息,请参阅第1部分,包括添加lodashquery-string 。 我们将使用lodash过滤数据和query-string ,这是获取Cosmic键的一种便捷方法,Cosmic键作为url参数提供。
  2. 同样, webpack.config.js.gitignore.babelrc将与第1部分完全相同。
  3. 除了新的配置变量方案之外, index.js不会更改:
  4. import React from ‘react’ 
    import ReactDom from ‘react-dom’ 
    import App from ‘./Components/App’ 
    import MuiThemeProvider from ‘material-ui/styles/MuiThemeProvider’ 
    import QueryString from ‘query-string’ 
    import ‘normalize.css’ 
    
    window .React = React 
    const url = QueryString.parse(location.search) 
    const config = { 
    	bucket : { 
    		slug : url.bucket_slug, 
    		write_key : url.write_key, 
    		read_key : url.read_key 
    	} 
    } 
    
    ReactDom.render( 
    	< MuiThemeProvider >  <App config={config}/> </MuiThemeProvider> , 
    	document.getElementById(‘root’) 
    )

3.锻炼骨骼

从这里开始,我们的范围和前端开始分化。

言归正传,我们的扩展程序将如下所示:

import { Component } from 'reac t'
import injectTapEventPlugin from 'react -tap-event-plugin'
injectTapEventPlugin()
export default class App extends Component  {
    constructor(props) {
        super (props)
      // set initial state
      // bind component methods
    }
  
  // component methods, lifecycle methods
  
  render() {
        return (
        //Material UI components 
        )
    }
}

考虑我们需要一个初始状态:

  • 我们从index.js url参数获取我们的Cosmic配置变量,并将它们作为props传递给App ,因此我们需要将它们移到其状态。
  • 我们将像在前端一样使用SnackBar ,因此我们需要跟踪其状态和消息。 我们还将使用DatePicker并且需要类似的策略。
  • 我们将提供一个带有下拉菜单的工具栏,用户可以在其中列出所有约会以进行日间过滤。 我们将通过在选择第一个状态变量时为状态变量分配1 ,为状态变量分配0来跟踪正在执行的操作。
  • 我们正在从Cosmic加载约会数据,因此对它们进行缓存将非常有用。 我们还需要单独执行此操作以按日期过滤约会。
  • 我们可以在约会表中选择行,并且需要跟踪选择的行。 跟踪被选中的所有行的状态也将很有用。

因此,对于初始状态,我们有:

this .state = {
  config : props.config,
  snackbarDisabled : false ,
  snackbarMessage : 'Loading...' ,
  toolbarDropdownValue : 1 ,
  appointments : {},
  filteredAppointments : {},
  datePickerDisabled : true ,
  selectedRows : [],
  deleteButtonDisabled : true ,
  allRowsSelected : false
}

4.草拟功能

我们的扩展程序需要具有以下功能才能使其按我们需要的方式工作:

  • componentWillMount() Cosmic JS中获取数据,并在单独的handleFetchMethod (以及它的伴随handleFetchError() )中进行处理。
  • 使用handleToobarDropdownChange()更改过滤器选项时更改状态
  • 使用handleRowSelection()覆盖默认的Material UI Table选择
  • 使用handleDelete()处理删除Appointment对象
  • 使用checkDisableDate()将禁用日期输入到DatePicker
  • 使用filterAppointments()过滤约会
  • 使用setTableChildren()约会呈现为TableRow

包括扩展名中的那些,我们现在有:

import { Component } from 'react'
import injectTapEventPlugin from 'react-tap-event-plugin'
injectTapEventPlugin()
export default class App extends Component {
    constructor(props) {
        super(props)
      this.state = {
          config: props.config,
          snackbarDisabled: false ,
          snackbarMessage: 'Loading...',
          toolbarDropdownValue: 1 ,
          appointments: {},
          filteredAppointments: {},
          datePickerDisabled: true ,
          selectedRows: [] ,
          deleteButtonDisabled: true ,
          allRowsSelected: false
        }
    }
  
  handle FetchError( err ) {
        //handle errors fetching data from Cosmic JS
    }
  
  handle Fetch( response ) {
        //process data fetched from Cosmic JS
    }
  
  handle ToolbarDropdownChange( val ) {
        // set the dropdown value and clear filteredAppointments() if
      // "List All" is selected. (State 1). 
    }
  
  handle 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.
    }
  
  handle Delete( selectedRows ) {
        //send a post request to Cosmic JS's api to get rid of unwanted appointments
    }
  
  check DisableDate( date ) {
        //feed the DatePicker days based on availability determined by appointments
      //retrieved from Cosmic
    }
  
  filter Appointments( date ) {
        //Only show appointments occuring on date
    }
  
 set TableChildren( selectedRows = this . state . selectedRows , appointments = this . state . appointments ) {
        //render a TableRow for each appointment loaded
    }
  
  component WillMount() {
        //fetch data immediately
    }
  
  render () {
        return (
        //Material UI components 
        )
    }
}

5.建立视图

像前端一样,我们将使用所有Material UI组件来显示数据。

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

6.获取约会数据

与前端不同,我们不必担心将敏感数据公开,因此我们可以轻松地使用cosmicjs来处理提取。 我们将在componentWillMount()进行此操作。

componentWillMount() {
  Cosmic.getObjectType(this .state.config, { type_slug : 'appointments' }, (err, response) => err ? this .handleFetchError(err) : this .handleFetch(response)
  )
}

我们将使用handleFetchError()处理错误,该错误将向用户显示SnackBar发生了错误。

handleFetchError(err) {console .log(err)
  this .setState({ snackbarMessage : 'Error loading data' })
}

如果成功返回数据,我们将使用handleFetch()处理它。

handleFetch(response) {const appointments = response.objects.all ? response.objects.all.reduce( ( currentAppointments, appointment ) => {
    const date = appointment.metadata.date
    if (!currentAppointments[date]) currentAppointments[date] = []
    const appointmentData = {
      slot : appointment.metadata.slot,
      name : appointment.title,
      email : appointment.metadata.email,
      phone : appointment.metadata.phone,
      slug : appointment.slug
    }
    currentAppointments[date].push(appointmentData)
    currentAppointments[date].sort( ( a,b ) => a.slot - b.slot)
    return currentAppointments
  }, {}) : {}
  this .setState({ appointments, snackbarDisabled : true , tableChildren : this .setTableChildren([], appointments) })
}

从存储桶发送的Appointment对象数组中,我们创建所有加载的约会,约会的时间表。 然后,我们将其保存到状态并将其传递给setTableChildren()以用于呈现Table

7.处理UI更改

我们需要一些简单的方法来处理工具栏中的过滤器下拉列表,选择行,过滤约会,将禁用日期的检查提供给DatePicker 。 从处理下拉过滤器开始, 0映射到按日期过滤约会, 1映射到列出所有约会。 为了列出所有内容,我们重置state.filteredAppointments

handleToolbarDropdownChange(val) {//0: filter by date, 1: list all
  val ? this .setState({ filteredAppointments : {}, datePickerDisabled : true , toolbarDropdownValue : 1 }) : this .setState({ toolbarDropdownValue : 0 , datePickerDisabled : false })
}

为了处理行选择,我们将选定的行保存到状态,根据选定的行设置表子级,如果选择了至少一行,则启用删除按钮。

handleRowSelection(rowsToSelect) {const allRows = [...Array( this .state.tableChildren.length).keys()]
  const allRowsSelected = rowsToSelect === 'all'
  const selectedRows = Array .isArray(rowsToSelect) ? rowsToSelect : allRowsSelected ? allRows : []
  const appointments = _.isEmpty( this .state.filteredAppointments) ? this .state.appointments : this .state.filteredAppointments
  const deleteButtonDisabled = selectedRows.length == 0
  const tableChildren = allRowsSelected ? this .setTableChildren([], appointments) : this .setTableChildren(selectedRows, appointments)
  this .setState({ selectedRows, deleteButtonDisabled, tableChildren })
}

对于禁用日期,我们仅在state.appointments.date (其中date = 'YYYY-DD-MM' )存在的情况下将它们激活。

checkDisableDate(day) {return ! this .state.appointments[moment(day).format( 'YYYY-DD-MM' )]
}

8.筛选约会并呈现表格

当用户将过滤器下拉列表更改为“ Filter By Date他们随后从日期选择器中选择一个日期。 选择日期后,日期选择器将触发filterAppointments()state.filteredAppoitments设置为子计划state.appointments[selectedDate]并将该子计划传递给setTableChildren()

filterAppointments(date) {const dateString = moment(date).format( 'YYYY-DD-MM' )
  const filteredAppointments = {}
  filteredAppointments[dateString] = this .state.appointments[dateString]
  this .setState({ filteredAppointments, tableChildren : this .setTableChildren([], filteredAppointments) })
}

filterAppointments() (或任何其他方法)调用setTableChildren()我们可以有选择地传递一组选定的行和一个appointments对象,或将其默认设置为state.selectedRowsstate.appointments 。 如果约会被过滤,我们将在渲染之前按时间对其进行排序。

setTableChildren(selectedRows =this .state.selectedRows, appointments = this .state.appointments) {
  const renderAppointment = ( date, appointment, index ) => {
    const { name, email, phone, slot } = appointment
    const rowSelected = selectedRows.includes(index)
    return < TableRow key = {index} selected = {rowSelected} >
       <TableRowColumn>{index}</TableRowColumn>
      <TableRowColumn>{name}</TableRowColumn>
      <TableRowColumn>{email}</TableRowColumn>
      <TableRowColumn>{phone}</TableRowColumn>
      <TableRowColumn>{moment(date, 'YYYY-DD-MM').format('M[/]D[/]YYYY')}</TableRowColumn>
      <TableRowColumn>{moment().hour(9).minute(0).add(slot, 'hours').format('h:mm a')}</TableRowColumn> 
    </ TableRow >
  }
  const appointmentsAreFiltered = !_.isEmpty( this .state.filteredAppointments)
  const schedule = appointmentsAreFiltered ? this .state.filteredAppointments : appointments
  const els = []
  let counter = 0
  appointmentsAreFiltered ?
    Object .keys(schedule).forEach( date => {
    schedule[date].forEach( ( appointment, index ) => els.push(renderAppointment(date, appointment, index)))
  }) :
  Object .keys(schedule).sort( ( a,b ) => moment(a, 'YYYY-DD-MM' ).isBefore(moment(b, 'YYYY-MM-DD' )))
    .forEach( ( date, index ) => {
    schedule[date].forEach( appointment => {
      els.push(renderAppointment(date, appointment, counter))
      counter++
    })
  })
  return els
}

9.删除约会

我们需要处理的最后一件事是让用户删除约会,利用cosmicjs删除对象使用发现lodashstate.appointments根据selectedRows

handleDelete(selectedRows) {const { config } = this .state
  return selectedRows.map( row => {
    const { tableChildren, appointments } = this .state
    const date = moment(tableChildren[row].props.children[ 4 ].props.children, 'M-D-YYYY' ).format( 'YYYY-DD-MM' )
    const slot = moment(tableChildren[row].props.children[ 5 ].props.children, 'h:mm a' ).diff(moment().hours( 9 ).minutes( 0 ).seconds( 0 ), 'hours' ) + 1
    return _.find(appointments[date], appointment =>
                  appointment.slot === slot
                 )
  }).map( appointment => appointment.slug).forEach( slug =>
                                                  Cosmic.deleteObject(config, { slug, write_key : config.bucket.write_key }, (err, response) => {
    if (err) {
      console .log(err)
      this .setState({ snackbarDisabled : false , snackbarMessage : 'Failed to delete appointments' })
    } else {
      this .setState({ snackbarMessage : 'Loading...' , snackbarDisabled : false })
      Cosmic.getObjectType( this .state.config, { type_slug : 'appointments' }, (err, response) =>
                           err ? this .handleFetchError(err) : this .handleFetch(response)
                          )}
  }
                                                                     )
                                                 )
  this .setState({ selectedRows : [], deleteButtonDisabled : true })
}

10.放在一起

至此,包括所有必要的导入,我们完成的扩展如下所示:

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

第5部分:结论

一旦您在appointment-scheduler-extension运行webpack ,就在dist创建extension.json以使Cosmic能够识别它:

// appointment-scheduler-extension/dist/extension.json
{"title" : "Appointment Manager" ,
  "font_awesome_class" : "fa-calendar" ,
  "image_url" : ""
}

然后,压缩dist ,将其上传到Cosmic,我们准备开始管理约会。

使用Cosmic JS,Twilio,Express和React,我们构建了一个模块化,易于扩展的约会计划程序,以使其他人可以轻松访问我们的时间,同时为自己节省更多时间。 我们能够部署应用程序的速度以及数据管理的简便性都增强了将Cosmic JS用于CMS和部署的明显选择。

尽管我们的约会计划程序肯定会在将来节省我们的时间,但可以肯定的是,它永远无法与Cosmic为我们节省未来项目的时间竞争。

Matt Cain构建了智能Web应用程序,并撰写了用于构建智能Web应用程序的技术。 您可以从他的 投资组合中 进一步了解他

翻译自: https://hackernoon.com/build-an-appointment-scheduler-using-react-twilio-and-cosmic-js-95377f6d1040

react调度时间原理

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值