Commit 07ce8d1c9520040bec94f1012e9ab63d690c18f4
1 parent
667ddb29bc
Exists in
master
New student form layout and inital controls
Showing
10 changed files
with
587 additions
and
2 deletions
Show diff stats
imports/client/views/core/ErrorLabel.js
... | ... | @@ -0,0 +1,17 @@ |
1 | +import React, { PropTypes } from 'react' | |
2 | + | |
3 | +const ErrorLabel = props => ( | |
4 | + <label | |
5 | + className="validation-error-label" | |
6 | + htmlFor={props.field} | |
7 | + > | |
8 | + {props.children} | |
9 | + </label> | |
10 | +) | |
11 | + | |
12 | +ErrorLabel.propTypes = { | |
13 | + field: PropTypes.string.isRequired, | |
14 | + children: PropTypes.string.isRequired, | |
15 | +} | |
16 | + | |
17 | +export default ErrorLabel | ... | ... |
imports/client/views/core/Form.js
... | ... | @@ -0,0 +1,144 @@ |
1 | +import React, { Component, PropTypes } from 'react' | |
2 | +import isEqual from 'lodash/isEqual' | |
3 | +import get from 'lodash/get' | |
4 | +import cloneDeep from 'lodash/cloneDeep' | |
5 | +import set from 'lodash/set' | |
6 | + | |
7 | +class Form extends Component { | |
8 | + constructor(props) { | |
9 | + super(props) | |
10 | + this.state = { | |
11 | + values: props.initialValues || {}, | |
12 | + dirtyValues: [], | |
13 | + submitted: false | |
14 | + } | |
15 | + this.isPristine = this.isPristine.bind(this) | |
16 | + this.isDirty = this.isDirty.bind(this) | |
17 | + this.isSubmitted = this.isSubmitted.bind(this) | |
18 | + this.getValue = this.getValue.bind(this) | |
19 | + this.setValue = this.setValue.bind(this) | |
20 | + this.setPristine = this.setPristine.bind(this) | |
21 | + this.setDirty = this.setDirty.bind(this) | |
22 | + } | |
23 | + | |
24 | + componentWillReceiveProps(nextProps) { | |
25 | + if (!isEqual(this.props.initialValues, nextProps.initialValues)) { | |
26 | + this.setState(prevState => ({ | |
27 | + values: { ...nextProps.initialValues, ...prevState.values } | |
28 | + })) | |
29 | + } | |
30 | + } | |
31 | + | |
32 | + isPristine(path) { | |
33 | + if (path) { | |
34 | + return !this.state.dirtyValues.find(dirtyValue => dirtyValue === path) | |
35 | + } | |
36 | + return !this.state.dirtyValues.length | |
37 | + } | |
38 | + | |
39 | + isDirty(path) { | |
40 | + return !this.isPristine(path) | |
41 | + } | |
42 | + | |
43 | + isSubmitted() { | |
44 | + return this.state.submitted | |
45 | + } | |
46 | + | |
47 | + getValue(path) { | |
48 | + if (!path) { | |
49 | + throw new Error(`getValue() requires a path`) | |
50 | + } | |
51 | + return get(this.state.values, path, '') | |
52 | + } | |
53 | + | |
54 | + setValue (path, value) { | |
55 | + if (!path) { | |
56 | + throw new Error(`setValue() requires a path`) | |
57 | + } | |
58 | + if (value === undefined) { | |
59 | + throw new Error(`setValue() requires a non-undefined value`) | |
60 | + } | |
61 | + this.setState(prevState => { | |
62 | + const prevValues = prevState.values | |
63 | + // Lo-Dash's set() mutates the original value, so we need to make a copy | |
64 | + const prevValuesCopy = cloneDeep(prevValues) | |
65 | + const nextValues = set(prevValuesCopy, path, value) | |
66 | + return { values: nextValues } | |
67 | + }) | |
68 | + this.setDirty(path) | |
69 | + } | |
70 | + | |
71 | + setPristine(path) { | |
72 | + if (!path) { | |
73 | + throw new Error(`setPristine() requires a path`) | |
74 | + } | |
75 | + this.setState(prevState => ({ | |
76 | + dirtyValues: ( | |
77 | + this.isPristine(path) | |
78 | + ? prevState.dirtyValues | |
79 | + : prevState.dirtyValues.filter(dirtyValue => dirtyValue !== path) | |
80 | + ) | |
81 | + })) | |
82 | + } | |
83 | + | |
84 | + setDirty(path) { | |
85 | + if (!path) { | |
86 | + throw new Error(`setDirty() requires a path`) | |
87 | + } | |
88 | + this.setState(prevState => ({ | |
89 | + dirtyValues: ( | |
90 | + this.isDirty(path) | |
91 | + ? prevState.dirtyValues | |
92 | + : [...prevState.dirtyValues, path] | |
93 | + ) | |
94 | + })) | |
95 | + } | |
96 | + | |
97 | + reset() { | |
98 | + this.setState({ | |
99 | + values: this.props.initialValues, | |
100 | + dirtyValues: [], | |
101 | + submitted: false | |
102 | + }) | |
103 | + } | |
104 | + | |
105 | + handleSubmit(event) { | |
106 | + if (event) { | |
107 | + event.preventDefault() | |
108 | + } | |
109 | + this.setState({ submitted: true }) | |
110 | + if (this.props.onSubmit) { | |
111 | + this.props.onSubmit(this.state.values) | |
112 | + } | |
113 | + } | |
114 | + | |
115 | + render() { | |
116 | + // eslint-disable-next-line | |
117 | + const { initialValues, children, ...rest } = this.props | |
118 | + return ( | |
119 | + <form {...rest} onSubmit={this.handleSubmit}> | |
120 | + {children({ | |
121 | + values: this.state.values, | |
122 | + isPristine: this.isPristine, | |
123 | + isDirty: this.isDirty, | |
124 | + isSubmitted: this.isSubmitted, | |
125 | + getValue: this.getValue, | |
126 | + setValue: this.setValue, | |
127 | + setPristine: this.setPristine, | |
128 | + setDirty: this.setDirty, | |
129 | + reset: this.reset | |
130 | + })} | |
131 | + </form> | |
132 | + ) | |
133 | + } | |
134 | +} | |
135 | + | |
136 | +Form.propTypes = { | |
137 | + initialValues: PropTypes.object, | |
138 | + onSubmit: PropTypes.func, | |
139 | + children: PropTypes.func.isRequired | |
140 | +} | |
141 | + | |
142 | +Form.defaultProps = { initialValues: {} } | |
143 | + | |
144 | +export default Form | ... | ... |
imports/client/views/core/Label.js
... | ... | @@ -0,0 +1,17 @@ |
1 | +import React, { PropTypes } from 'react' | |
2 | + | |
3 | +const Label = props => ( | |
4 | + <label> | |
5 | + {props.children} | |
6 | + {props.required && ( | |
7 | + <span className="text-danger">*</span> | |
8 | + )} | |
9 | + </label> | |
10 | +) | |
11 | + | |
12 | +Label.propTypes = { | |
13 | + children: PropTypes.string.isRequired, | |
14 | + required: PropTypes.bool, | |
15 | +} | |
16 | + | |
17 | +export default Label | ... | ... |
imports/client/views/core/Stepper.js
... | ... | @@ -0,0 +1,21 @@ |
1 | +import React, { PropTypes } from 'react' | |
2 | + | |
3 | +const Stepper = props => ( | |
4 | + <ul className="stepy-header"> | |
5 | + {props.steps.map((step, index) => ( | |
6 | + <li key={step.label} className={step.active && 'stepy-active'}> | |
7 | + <div>{index + 1}</div> | |
8 | + {step.label} | |
9 | + </li> | |
10 | + ))} | |
11 | + </ul> | |
12 | +) | |
13 | + | |
14 | +Stepper.propTypes = { | |
15 | + steps: PropTypes.arrayOf(PropTypes.shape({ | |
16 | + label: PropTypes.string.isRequired, | |
17 | + active: PropTypes.bool.isRequired, | |
18 | + })) | |
19 | +} | |
20 | + | |
21 | +export default Stepper | ... | ... |
imports/client/views/core/Validator.js
... | ... | @@ -0,0 +1,48 @@ |
1 | +import { Component, PropTypes } from 'react' | |
2 | +import isArray from 'lodash/isArray' | |
3 | +import get from 'lodash/get' | |
4 | + | |
5 | +class Validator extends Component { | |
6 | + constructor(props) { | |
7 | + super(props) | |
8 | + this.getErrors = this.getErrors.bind(this) | |
9 | + } | |
10 | + | |
11 | + getErrors() { | |
12 | + if (!this.props.validations) { | |
13 | + return null | |
14 | + } | |
15 | + const errors = Object.keys(this.props.validations).reduce((result, path) => { | |
16 | + const validations = this.props.validations[path] | |
17 | + if (!isArray(validations)) { | |
18 | + throw new Error(`validations[${path}] should be an array`) | |
19 | + } | |
20 | + return { | |
21 | + ...result, | |
22 | + [path]: validations.map((validation, index) => { | |
23 | + if (typeof validation !== 'function') { | |
24 | + throw new Error(`validations[${path}][${index}] should be a function`) | |
25 | + } | |
26 | + const error = validation(get(this.props.values, path, ''), this.props.values, path) | |
27 | + if (error && typeof error !== 'string') { | |
28 | + throw new Error(`validations[${path}][${index}] should return a string`) | |
29 | + } | |
30 | + return error | |
31 | + }).find(error => error) || null | |
32 | + } | |
33 | + }, {}) | |
34 | + return Object.keys(errors).find(path => errors[path]) ? errors : null | |
35 | + } | |
36 | + | |
37 | + render() { | |
38 | + return this.props.children({ errors: this.getErrors() }) | |
39 | + } | |
40 | +} | |
41 | + | |
42 | +Validator.propTypes = { | |
43 | + validations: PropTypes.object, | |
44 | + values: PropTypes.object, | |
45 | + children: PropTypes.func.isRequired | |
46 | +} | |
47 | + | |
48 | +export default Validator | ... | ... |
imports/client/views/core/validations.js
... | ... | @@ -0,0 +1,10 @@ |
1 | +export const isRequired = value => !value && 'Required' | |
2 | + | |
3 | +export const minLength = minLength => { | |
4 | + return value => value && value.length < minLength && `Min. length: ${minLength}` | |
5 | +} | |
6 | + | |
7 | +export const isValidEmail = value => { | |
8 | + const exR = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/ | |
9 | + return !exR.test(value) && 'Please use a valid email' | |
10 | +} | ... | ... |
imports/client/views/org/admin/students/AddStudentFormContainer.js
... | ... | @@ -0,0 +1,56 @@ |
1 | +import React, { Component } from 'react' | |
2 | +import { AddStudentForm } from './addStudentForm' | |
3 | +import StudentForm from './StudentForm' | |
4 | +import Form from '../../../core/Form' | |
5 | +import Validator from '../../../core/Validator' | |
6 | +import { isRequired } from '../../../core/validations' | |
7 | + | |
8 | +export class AddStudentFormContainer extends Component { | |
9 | + | |
10 | + constructor(props) { | |
11 | + super(props) | |
12 | + this.state = { currentStep: 0 } | |
13 | + this.handleNextClick = this.handleNextClick.bind(this) | |
14 | + this.handleBackClick = this.handleBackClick.bind(this) | |
15 | + } | |
16 | + | |
17 | + handleNextClick() { | |
18 | + this.form.handleSubmit() | |
19 | + if (this.validator.getErrors() && Object.keys(this.validator.getErrors()).length > 0) return | |
20 | + this.setState({ currentStep: this.state.currentStep + 1 }) | |
21 | + this.form.reset() | |
22 | + } | |
23 | + | |
24 | + handleBackClick() { | |
25 | + this.setState({ currentStep: this.state.currentStep + -1 }) | |
26 | + } | |
27 | + | |
28 | + render() { | |
29 | + return ( | |
30 | + <Form onSubmit={() => alert('submitted')} ref={form => this.form = form}> | |
31 | + {({ values, setValue, getValue, isSubmitted, isDirty }) => ( | |
32 | + <Validator | |
33 | + values={values} | |
34 | + ref={validator => this.validator = validator} | |
35 | + validations={{ | |
36 | + admissionId: [isRequired], | |
37 | + }} | |
38 | + > | |
39 | + {({ errors }) => ( | |
40 | + <StudentForm | |
41 | + isDirty={isDirty} | |
42 | + setValue={setValue} | |
43 | + getValue={getValue} | |
44 | + isSubmitted={isSubmitted} | |
45 | + errors={errors} | |
46 | + onNextClick={this.handleNextClick} | |
47 | + onBackClick={this.handleBackClick} | |
48 | + currentStep={this.state.currentStep} | |
49 | + /> | |
50 | + )} | |
51 | + </Validator> | |
52 | + )} | |
53 | + </Form> | |
54 | + ) | |
55 | + } | |
56 | +} | ... | ... |
imports/client/views/org/admin/students/StudentForm.js
... | ... | @@ -0,0 +1,271 @@ |
1 | +import React, { PropTypes } from 'react' | |
2 | +import { | |
3 | + Row, | |
4 | + Col, | |
5 | + FormGroup, | |
6 | + FormControl, | |
7 | + Button | |
8 | +} from 'react-bootstrap' | |
9 | +import DatePicker from 'react-bootstrap-date-picker' | |
10 | +import Label from '../../../core/Label' | |
11 | +import Stepper from '../../../core/Stepper' | |
12 | +import ErrorLabel from '../../../core/ErrorLabel' | |
13 | + | |
14 | +const StudentForm = props => ( | |
15 | + <div className="stepy-validation"> | |
16 | + <Stepper | |
17 | + steps={[ | |
18 | + { | |
19 | + label: 'Personal data', | |
20 | + active: props.currentStep === 0, | |
21 | + }, | |
22 | + { | |
23 | + label: 'Address', | |
24 | + active: props.currentStep === 1, | |
25 | + }, | |
26 | + { | |
27 | + label: 'Parent info', | |
28 | + active: props.currentStep === 2, | |
29 | + } | |
30 | + ]} | |
31 | + /> | |
32 | + {props.currentStep === 0 && ( | |
33 | + <fieldset title="1"> | |
34 | + <legend className="text-semibold">Personal data</legend> | |
35 | + <Row> | |
36 | + <Col xs={12} sm={6}> | |
37 | + <FormGroup controlId="admissionId"> | |
38 | + <Label required>Admission Id</Label> | |
39 | + <FormControl | |
40 | + type="text" | |
41 | + value={props.getValue('admissionId')} | |
42 | + placeholder="admission Id" | |
43 | + onChange={e => props.setValue('admissionId', e.target.value)} | |
44 | + /> | |
45 | + {props.isSubmitted() && props.errors && props.errors.admissionId && ( | |
46 | + <ErrorLabel> {props.errors.admissionId} </ErrorLabel> | |
47 | + )} | |
48 | + </FormGroup> | |
49 | + </Col> | |
50 | + <Col xs={12} sm={6}> | |
51 | + <FormGroup controlId="firstName"> | |
52 | + <Label>First Name</Label> | |
53 | + <FormControl | |
54 | + type="text" | |
55 | + value={props.getValue('firstName')} | |
56 | + placeholder="First Name" | |
57 | + onChange={e => props.setValue('firstName', e.target.value)} | |
58 | + /> | |
59 | + </FormGroup> | |
60 | + </Col> | |
61 | + </Row> | |
62 | + <Row> | |
63 | + <Col xs={12} sm={6}> | |
64 | + <FormGroup controlId="middleName"> | |
65 | + <Label>Middle Name</Label> | |
66 | + <FormControl | |
67 | + type="text" | |
68 | + value={props.getValue('middleName')} | |
69 | + placeholder="Middle Name" | |
70 | + onChange={e => props.setValue('middleName', e.target.value)} | |
71 | + /> | |
72 | + </FormGroup> | |
73 | + </Col> | |
74 | + <Col xs={12} sm={6}> | |
75 | + <FormGroup controlId="lastName"> | |
76 | + <Label>Last Name</Label> | |
77 | + <FormControl | |
78 | + type="text" | |
79 | + value={props.getValue('lastName')} | |
80 | + placeholder="Last Name" | |
81 | + onChange={e => props.setValue('lastName', e.target.value)} | |
82 | + /> | |
83 | + </FormGroup> | |
84 | + </Col> | |
85 | + </Row> | |
86 | + <Row> | |
87 | + <Col xs={12} sm={6}> | |
88 | + <FormGroup controlId="email"> | |
89 | + <Label>Email</Label> | |
90 | + <FormControl | |
91 | + type="email" | |
92 | + value={props.getValue('email')} | |
93 | + placeholder="Email" | |
94 | + onChange={e => props.setValue('email', e.target.value)} | |
95 | + /> | |
96 | + </FormGroup> | |
97 | + </Col> | |
98 | + <Col xs={12} sm={6}> | |
99 | + <FormGroup> | |
100 | + <Label>Date of birth</Label> | |
101 | + <DatePicker | |
102 | + id="dob" | |
103 | + value={props.getValue('dob')} | |
104 | + onChange={value => props.setValue('dob', value)} | |
105 | + /> | |
106 | + </FormGroup> | |
107 | + </Col> | |
108 | + </Row> | |
109 | + <Row> | |
110 | + <Col xs={12} sm={6}> | |
111 | + <FormGroup controlId="formControlsSelect"> | |
112 | + <Label>Gender</Label> | |
113 | + <FormControl componentClass="select" | |
114 | + placeholder="select" | |
115 | + value={props.getValue('gender')} | |
116 | + onChange={e => props.setValue('gender', e.target.value)} | |
117 | + > | |
118 | + <option value="male">Male</option> | |
119 | + <option value="female">Female</option> | |
120 | + </FormControl> | |
121 | + </FormGroup> | |
122 | + </Col> | |
123 | + </Row> | |
124 | + </fieldset> | |
125 | + )} | |
126 | + {props.currentStep === 1 && ( | |
127 | + <fieldset title="2"> | |
128 | + <legend className="text-semibold">Address</legend> | |
129 | + <Row> | |
130 | + <Col xs={12} sm={6}> | |
131 | + <FormGroup controlId="rollNo"> | |
132 | + <Label>Roll No</Label> | |
133 | + <FormControl | |
134 | + type="text" | |
135 | + value={props.getValue('rollNo')} | |
136 | + placeholder="Roll No" | |
137 | + onChange={e => props.setValue('rollNo', e.target.value)} | |
138 | + /> | |
139 | + </FormGroup> | |
140 | + </Col> | |
141 | + <Col xs={12} sm={6}> | |
142 | + <FormGroup controlId="class"> | |
143 | + <Label>Class</Label> | |
144 | + <FormControl | |
145 | + type="text" | |
146 | + value={props.getValue('class')} | |
147 | + placeholder="7" | |
148 | + onChange={e => props.setValue('class', e.target.value)} | |
149 | + /> | |
150 | + </FormGroup> | |
151 | + </Col> | |
152 | + </Row> | |
153 | + <Row> | |
154 | + <Col xs={12} sm={6}> | |
155 | + <FormGroup controlId="section"> | |
156 | + <Label>Section</Label> | |
157 | + <FormControl | |
158 | + type="text" | |
159 | + value={props.getValue('section')} | |
160 | + placeholder="B" | |
161 | + onChange={e => props.setValue('section', e.target.value)} | |
162 | + /> | |
163 | + </FormGroup> | |
164 | + </Col> | |
165 | + <Col xs={12} sm={6}> | |
166 | + <FormGroup controlId="community"> | |
167 | + <Label>Community</Label> | |
168 | + <FormControl | |
169 | + type="text" | |
170 | + value={props.getValue('community')} | |
171 | + placeholder="General" | |
172 | + onChange={e => props.setValue('community', e.target.value)} | |
173 | + /> | |
174 | + </FormGroup> | |
175 | + </Col> | |
176 | + </Row> | |
177 | + <Row> | |
178 | + <Col xs={12} sm={6}> | |
179 | + <FormGroup controlId="bloodGroup"> | |
180 | + <Label>bloodGroup</Label> | |
181 | + <FormControl | |
182 | + type="text" | |
183 | + value={props.getValue('bloodGroup')} | |
184 | + placeholder="B+" | |
185 | + onChange={e => props.setValue('bloodGroup', e.target.value)} | |
186 | + /> | |
187 | + </FormGroup> | |
188 | + </Col> | |
189 | + <Col xs={12} sm={6}> | |
190 | + <FormGroup controlId="phone"> | |
191 | + <Label>Phone</Label> | |
192 | + <FormControl | |
193 | + type="text" | |
194 | + value={props.getValue('phone')} | |
195 | + placeholder="9999999999" | |
196 | + onChange={e => props.setValue('phone', e.target.value)} | |
197 | + /> | |
198 | + </FormGroup> | |
199 | + </Col> | |
200 | + </Row> | |
201 | + <Row> | |
202 | + <Col xs={12} sm={6}> | |
203 | + <FormGroup controlId="address"> | |
204 | + <Label>Address</Label> | |
205 | + <FormControl | |
206 | + type="text" | |
207 | + value={props.getValue('address')} | |
208 | + placeholder="#876, Street, town" | |
209 | + onChange={e => props.setValue('address', e.target.value)} | |
210 | + /> | |
211 | + </FormGroup> | |
212 | + </Col> | |
213 | + <Col xs={12} sm={6}> | |
214 | + <FormGroup controlId="city"> | |
215 | + <Label>City</Label> | |
216 | + <FormControl | |
217 | + type="text" | |
218 | + value={props.getValue('city')} | |
219 | + placeholder="Chennai" | |
220 | + onChange={e => props.setValue('city', e.target.value)} | |
221 | + /> | |
222 | + </FormGroup> | |
223 | + </Col> | |
224 | + </Row> | |
225 | + <Row> | |
226 | + <Col xs={12} sm={6}> | |
227 | + <FormGroup controlId="state"> | |
228 | + <Label>State</Label> | |
229 | + <FormControl | |
230 | + type="text" | |
231 | + value={props.getValue('state')} | |
232 | + placeholder="Tamilnadu" | |
233 | + onChange={e => props.setValue('state', e.target.value)} | |
234 | + /> | |
235 | + </FormGroup> | |
236 | + </Col> | |
237 | + </Row> | |
238 | + </fieldset> | |
239 | + )} | |
240 | + <div style={{ textAlign: 'left' }}> | |
241 | + {props.currentStep > 0 && ( | |
242 | + <div style={{ display: 'inline-block', marginRight: 10 }}> | |
243 | + <Button onClick={props.onBackClick}> | |
244 | + <i className="icon-arrow-left13 position-left"></i> | |
245 | + BACK | |
246 | + </Button> | |
247 | + | |
248 | + </div> | |
249 | + )} | |
250 | + <div style={{ display: 'inline-block' }}> | |
251 | + <Button | |
252 | + bsStyle="primary" | |
253 | + onClick={props.onNextClick} | |
254 | + > | |
255 | + NEXT | |
256 | + <i className="icon-arrow-right14 position-right" /> | |
257 | + </Button> | |
258 | + </div> | |
259 | + </div> | |
260 | + </div> | |
261 | +) | |
262 | + | |
263 | +StudentForm.propTypes = { | |
264 | + currentStep: PropTypes.number.isRequired, | |
265 | + onNextClick: PropTypes.func.isRequired, | |
266 | + onBackClick: PropTypes.func.isRequired, | |
267 | + setValue: PropTypes.func.isRequired, | |
268 | + getValue: PropTypes.func.isRequired, | |
269 | +} | |
270 | + | |
271 | +export default StudentForm | ... | ... |
imports/client/views/org/admin/students/addStudent.js
... | ... | @@ -6,7 +6,7 @@ import { Link,browserHistory } from 'react-router'; |
6 | 6 | import { FormGroup,Panel,Table, |
7 | 7 | ButtonToolbar,Modal, |
8 | 8 | FormControl,Glyphicon,Button } from 'react-bootstrap'; |
9 | -import { AddStudentForm } from './addStudentForm'; | |
9 | +import { AddStudentFormContainer } from './AddStudentFormContainer'; | |
10 | 10 | |
11 | 11 | const style = { |
12 | 12 | margin: 12, |
... | ... | @@ -49,7 +49,7 @@ export class AddStudent extends Component { |
49 | 49 | <Modal.Title id="contained-modal-title-lg">New Student</Modal.Title> |
50 | 50 | </Modal.Header> |
51 | 51 | <Modal.Body> |
52 | - <AddStudentForm /> | |
52 | + <AddStudentFormContainer /> | |
53 | 53 | </Modal.Body> |
54 | 54 | <Modal.Footer> |
55 | 55 | <Button onClick={this.hideModal}>Close</Button> | ... | ... |
package.json