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。
预约时间
在这里, email
和phone
将成为用户的元数据-我们将使用他们的名称作为约会对象的标题。 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
}
注意appointmentMeridiem
取0
或1
,这样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之后的应用程序导航。 - 在
Drawer
,MenuItem
用于显示链接 -
Card
作为主要内容容器 -
Stepper
将调度过程分为3个谨慎的步骤。 活动步骤将被扩展,而其他步骤将被折叠。 如果state.loading
为true,则将禁用state.loading
,并且只要用户尚未填写上一步,则将禁用后两步。 - 嵌套在
Stepper
,三个Steps
包含StepButton
和StepContent
组件。 - 在
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.configs
和response.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 < 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 < 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) => {
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 => 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 => this. set State({ confirmationSnackbarMessage: "Appointment succesfully added!" , confirmationSnackbarOpen: true, processed: true }))
.catch(err => {
console. log (err)
return this. set State({ confirmationSnackbarMessage: "Appointment failed to save." , confirmationSnackbarOpen: true })
})
}</code>
<code class="language-markup"> validateEmail(email) {
const regex = /^(([^<>()\[ \] \. ,;: \s @ \" ]+( \. [^<>() \[ \] \. ,;: \s @ \" ]+)*)|( \" .+ \" ))@(([^<>()[ \] \. ,;: \s @ \" ]+ \. )+[^<>()[ \] \. ,;: \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' )) < 0
}</code>
<codeclass =" language - markup "> 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</code>
<code class="language-markup"> < ;span style={spanStyle} > ; 1 hour < ;/span > ; </code>
<code class="language-markup" > 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
}</code>
<codeclass =" language - markup "> 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
}
}</code>
<code class="language-markup" > renderAppointmentConfirmati on( ) {
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 > ;
}</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 =>
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)
}</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 & ; & ; 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 > ;</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-parser
,cors
,http
,morgan
,path
和express
-
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
您的TwilioTWILIO_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部分,包括添加
lodash
和query-string
。 我们将使用lodash
过滤数据和query-string
,这是获取Cosmic键的一种便捷方法,Cosmic键作为url参数提供。 - 同样,
webpack.config.js
,.gitignore
和.babelrc
将与第1部分完全相同。 - 除了新的配置变量方案之外,
index.js
不会更改: 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 UITable
选择 - 使用
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.selectedRows
和state.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
删除对象使用发现lodash
从state.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调度时间原理