diff --git a/ashes/src/components/custom-properties/custom-properties.css b/ashes/src/components/custom-properties/custom-properties.css new file mode 100644 index 0000000000..348f0289c3 --- /dev/null +++ b/ashes/src/components/custom-properties/custom-properties.css @@ -0,0 +1,50 @@ + +.controls { + position: absolute; + right: 21px; + + & > i { + margin: 0 5px; + cursor: pointer; + } +} + +:global .fc-save-cancel { + display: flex; + justify-content: flex-end; +} + +:global .fc-save-cancel__cancel { + background-color: transparent; + text-decoration: underline; + + &:hover { + background-color: transparent; + } +} + +.addProperty { + display: flex; + align-items: center; + margin: 30px -20px -20px -20px; + padding: 20px; + border-top: 1px solid #d9d9d9; + cursor: pointer; + transition: all .2s; + + &:hover { + background-color: #f7f7f7; + } +} + +.buttonTitle { + margin-right: 10px; + font-size: 14px; + font-weight: 600; +} + +.icon { + font-size: 10px; +} + + diff --git a/ashes/src/components/custom-properties/custom-properties.jsx b/ashes/src/components/custom-properties/custom-properties.jsx new file mode 100644 index 0000000000..30e8c3abc5 --- /dev/null +++ b/ashes/src/components/custom-properties/custom-properties.jsx @@ -0,0 +1,247 @@ +// @flow + +// libs +import React, { Component, Element } from 'react'; +import { autobind } from 'core-decorators'; +import { get, keys, omit } from 'lodash'; + +// components +import CustomPropertyModal from './custom-property-modal'; +import ConfirmationDialog from '../modal/confirmation-dialog'; + +// style +import s from './custom-properties.css'; + +type Props = { + canAddProperty?: boolean, + attributes: Attributes, + onChange: (attributes: Attributes) => void, + schema?: Object, + children: Element<*> +}; + +type State = { + isAddingProperty: boolean, + isEditingProperty: boolean, + isDeletingProperty: boolean, + errors: { [id: string]: any }, + currentEdit: { + name: string, + type: string, + value: any, + }, + propertyToDelete: string, +} + +export default class CustomProperties extends Component { + props: Props; + + state: State = { + isAddingProperty: false, + isEditingProperty: false, + isDeletingProperty: false, + errors: {}, + currentEdit: { + name: '', + type: '', + value: '', + }, + propertyToDelete: '', + }; + + get customPropertyForm() { + if (this.state.isAddingProperty) { + return ( + this.setState({ isAddingProperty: false })} + /> + ); + } + + if (this.state.isEditingProperty) { + return ( + this.setState({ isEditingProperty: false })} + /> + ); + } + } + + get deletePropertyForm() { + if (this.state.isDeletingProperty) { + return ( + this.setState({ isDeletingProperty: false }) } + /> + ); + } + } + + @autobind + controlButtons(name: string, type: string, value: any) { + const defaultProperties = keys(get(this.props.schema, 'properties', {})); + + if (defaultProperties.includes(name)) { + return null; + } + + return ( +
+ this.onEdit(name, type, value)} /> + this.onDelete(name)} /> +
+ ); + } + + @autobind + handleCreateProperty(property: { fieldLabel: string, propertyType: string }) { + const { fieldLabel, propertyType } = property; + const label = fieldLabel.toLowerCase(); + // TODO show error message, if fieldLabel is not unique + if (!this.isUnique(label)) { + return null; + } + + const value = (() => { + switch (propertyType) { + case('date'): + return new Date().toString(); + case('bool'): + return false; + default: + return ''; + } + })(); + this.setState({ + isAddingProperty: false + }, () => this.handleChange(label, propertyType, value)); + } + + @autobind + handleEditProperty(property: { fieldLabel: string, propertyType: string, fieldValue: any }) { + const { attributes } = this.props; + const { currentEdit: { name } } = this.state; + const { fieldLabel, propertyType, fieldValue } = property; + + const preparedObject = omit(attributes, name); + const newAttributes = { + ...preparedObject, + [fieldLabel]: { + t: propertyType, + v: fieldValue, + } + }; + + this.setState({ + isEditingProperty: false, + currentEdit: { + name: '', + type: '', + value: '' + } + }, this.props.onChange(newAttributes)); + } + + @autobind + handleDeleteProperty() { + const newAttributes = omit(this.props.attributes, this.state.propertyToDelete); + this.setState({ isDeletingProperty: false }, this.props.onChange(newAttributes)); + } + + @autobind + handleChange(name: string, type: string, value: any) { + const { attributes } = this.props; + const newAttributes = { + ...attributes, + [name]: { + t: type, + v: value, + } + }; + + this.props.onChange(newAttributes); + } + + @autobind + isUnique(fieldLabel: string) { + const reservedNames = keys(get(this.props, 'attributes', {})); + const unique = !reservedNames.includes(fieldLabel); + return unique; + } + + @autobind + processAttr(content: Element<*>, name: string, type: string, value: any) { + return ( +
+ {this.controlButtons(name, type, value)} + {content} +
+ ); + } + + @autobind + handleAddProperty() { + this.setState({ isAddingProperty: true }); + } + + @autobind + onEdit(name: string, type: string, value: any) { + this.setState({ + isEditingProperty: true, + currentEdit: { + name, + type, + value + }, + }); + } + + @autobind + onDelete(name: string) { + this.setState({ + isDeletingProperty: true, + propertyToDelete: name + }); + } + + get addCustomProperty() { + if (this.props.canAddProperty) { + return ( +
+ Custom Property + + + +
+ ); + } + } + + get children(): Element<*> { + return React.cloneElement((this.props.children), { + processAttr: this.processAttr, + }); + } + + render() { + return ( +
+ {this.children} + {this.addCustomProperty} + {this.customPropertyForm} + {this.deletePropertyForm} +
+ ); + } +} diff --git a/ashes/src/components/custom-properties/custom-property-modal.css b/ashes/src/components/custom-properties/custom-property-modal.css new file mode 100644 index 0000000000..d830d88f8a --- /dev/null +++ b/ashes/src/components/custom-properties/custom-property-modal.css @@ -0,0 +1,12 @@ +.modal { + min-width: 575px; +} + +:global .fc-dropdown { + max-width: 100%; + margin-bottom: 10px; +} + +:global .fc-content-box { + margin-bottom: 0; +} diff --git a/ashes/src/components/products/custom-property.jsx b/ashes/src/components/custom-properties/custom-property-modal.jsx similarity index 57% rename from ashes/src/components/products/custom-property.jsx rename to ashes/src/components/custom-properties/custom-property-modal.jsx index cc26fba0b8..2e9a507866 100644 --- a/ashes/src/components/products/custom-property.jsx +++ b/ashes/src/components/custom-properties/custom-property-modal.jsx @@ -3,16 +3,20 @@ */ // libs -import React, { Component, Element } from 'react'; +import { isEmpty, map, upperFirst } from 'lodash'; import { autobind } from 'core-decorators'; -import _ from 'lodash'; +import React, { Component, Element } from 'react'; // components -import { Dropdown } from '../dropdown'; -import { FormField } from '../forms'; -import wrapModal from '../modal/wrapper'; -import ContentBox from '../content-box/content-box'; +import { Dropdown } from 'components/dropdown'; +import { FormField } from 'components/forms'; +import wrapModal from 'components/modal/wrapper'; +import ContentBox from 'components/content-box/content-box'; import SaveCancel from 'components/core/save-cancel'; +import renderers from 'components/object-form/renderers'; + +//style +import s from './custom-property-modal.css'; const propertyTypes = { string: 'Text', @@ -20,28 +24,42 @@ const propertyTypes = { date: 'Date', price: 'Price', bool: 'Yes/No', + color: 'Color', + image: 'Image' }; type Props = { - onSave: (state: State) => void, - onCancel: () => void, + onSave: (state: State) => any, + onCancel: () => any, + currentEdit?: { + name: string, + type: string, + value: any, + } }; type State = { fieldLabel: string, propertyType: string, + fieldValue: any, }; -class CustomProperty extends Component { +class CustomPropertyModal extends Component { props: Props; - state: State; + + state: State = { + fieldLabel: '', + propertyType: '', + fieldValue: '', + }; constructor(props: Props) { super(props); - this.state = { - fieldLabel: '', - propertyType: '', - }; + Object.keys(propertyTypes).map((type) => { + if (Object.keys(renderers).indexOf(`render${upperFirst(type)}`) === -1) { + console.warn(`Custom property type: "${type}", does not have renderer!`); + } + }); } componentDidMount() { @@ -49,6 +67,14 @@ class CustomProperty extends Component { if (fieldLabelInput) { fieldLabelInput.focus(); } + + if (this.props.currentEdit) { + this.setState({ + fieldLabel: this.props.currentEdit.name, + propertyType: this.props.currentEdit.type, + fieldValue: this.props.currentEdit.value, + }); + } } get closeAction(): Element<*> { @@ -56,21 +82,37 @@ class CustomProperty extends Component { } get propertyTypes(): Array> { - return _.map(propertyTypes, (type, key) => [key, type]); + return map(propertyTypes, (type, key) => [key, type]); } get saveDisabled(): boolean { - return _.isEmpty(this.state.fieldLabel) || _.isEmpty(this.state.propertyType); + return isEmpty(this.state.fieldLabel) || isEmpty(this.state.propertyType); } @autobind - handleUpdateLabel({target}) { + handleUpdateLabel({ target }) { this.setState({ fieldLabel: target.value }); } @autobind handleUpdateType(value) { - this.setState({ propertyType: value }); + const fieldValue = (() => { + switch (value) { + case('date'): + return new Date().toString(); + case('bool'): + return false; + case('image'): + return {}; + default: + return ''; + } + })(); + + this.setState({ + propertyType: value, + fieldValue: fieldValue + }); } @autobind @@ -80,7 +122,7 @@ class CustomProperty extends Component { } @autobind - handleKeyPress(event){ + handleKeyPress(event) { if (!this.saveDisabled && event.keyCode === 13 /*enter*/) { event.preventDefault(); this.props.onSave(this.state); @@ -88,10 +130,12 @@ class CustomProperty extends Component { } render() { + const title = this.props.currentEdit ? 'Edit Custom Property' : 'New Custom Property'; + return ( -
+
- + { } } -const Wrapped: Class> = wrapModal(CustomProperty); +const Wrapped: Class> = wrapModal(CustomPropertyModal); export default Wrapped; diff --git a/ashes/src/components/image-card/image-card.css b/ashes/src/components/image-card/image-card.css index 0a65962805..b20f33f89d 100644 --- a/ashes/src/components/image-card/image-card.css +++ b/ashes/src/components/image-card/image-card.css @@ -21,6 +21,7 @@ .actions { position: absolute; width: 100%; + height: 60px; background: #333a44; opacity: .86; text-align: right; @@ -34,7 +35,7 @@ color: #fff; opacity: .76; font-size: 18px; - margin: 23px 10px; + margin: 18px 10px; cursor: pointer; transition: opacity .2s; diff --git a/ashes/src/components/image-card/image-card.jsx b/ashes/src/components/image-card/image-card.jsx index 14d955b32b..06c966700b 100644 --- a/ashes/src/components/image-card/image-card.jsx +++ b/ashes/src/components/image-card/image-card.jsx @@ -10,7 +10,7 @@ import { autobind } from 'core-decorators'; import React, { Component, Element } from 'react'; // components -import Image from '../image/image'; +import ImageLoader from '../image/image'; export type Action = { name: string, @@ -21,10 +21,11 @@ type Props = { id: number, src: string, actions: Array, - title: string, + title?: string, loading: boolean, secondaryTitle?: string, className?: string, + imageComponent?: string, }; type State = { @@ -96,13 +97,16 @@ export default class ImageCard extends Component { ); } - render() { - const { id, src, className } = this.props; + get imageLoader(): ?Element<*> { + const { id, src, imageComponent, loading } = this.props; + return !loading ? : null; + } + render() { return ( -
+
- + { this.imageLoader }
{this.actions} {this.description} diff --git a/ashes/src/components/image/image.jsx b/ashes/src/components/image/image.jsx index a18b60997d..06b61d1011 100644 --- a/ashes/src/components/image/image.jsx +++ b/ashes/src/components/image/image.jsx @@ -11,12 +11,12 @@ import Transition from 'react-transition-group/CSSTransitionGroup'; // components import WaitAnimation from '../common/wait-animation'; -import ProductImage from 'components/imgix/product-image'; type Props = { id: number, src: string, - loader?: string|Element<*>; + loader?: string | Element<*>, + imageComponent: string, } type State = { @@ -33,6 +33,10 @@ export default class ImageLoader extends Component { error: false, }; + static defaultProps = { + imageComponent: 'img', + }; + img: ?Image; showTransition: boolean = true; @@ -94,14 +98,18 @@ export default class ImageLoader extends Component { } get image(): ?Element<*> { - return this.state.ready ? ( - - ) : null; + ); } wrapToTransition(img: ?Element<*>) { diff --git a/ashes/src/components/images/album.jsx b/ashes/src/components/images/album.jsx index 35c18d1c51..925852b5cb 100644 --- a/ashes/src/components/images/album.jsx +++ b/ashes/src/components/images/album.jsx @@ -8,8 +8,9 @@ import { autobind, debounce } from 'core-decorators'; import React, { Component, Element } from 'react'; // components -import ConfirmationDialog from '../modal/confirmation-dialog'; -import Alert from '../alerts/alert'; +import ConfirmationDialog from 'components/modal/confirmation-dialog'; +import Alert from 'components/alerts/alert'; +import ProductImage from 'components/imgix/product-image'; import AlbumWrapper from './album-wrapper/album-wrapper'; import EditAlbum from './edit-album'; import Upload from '../upload/upload'; @@ -51,7 +52,7 @@ export default class Album extends Component { }; _uploadRef: Upload; - idsToKey: { [key:any]: string }; + idsToKey: { [key: any]: string }; constructor(...args: Array) { super(...args); @@ -137,12 +138,13 @@ export default class Album extends Component { const { album, loading } = this.props; return ( - ); } @@ -163,14 +165,15 @@ export default class Album extends Component { ); return ( - ); } @@ -194,12 +197,13 @@ export default class Album extends Component { onDrop={this.handleNewFiles} empty={album.images.length == 0} > - {album.images.map((image: ImageFile, idx: number) => { if (image.key && image.id) this.idsToKey[image.id] = image.key; @@ -210,6 +214,7 @@ export default class Album extends Component { imagePid={imagePid} editImage={(form: ImageInfo) => this.props.editImage(idx, form)} deleteImage={() => this.props.deleteImage(idx)} + imageComponent={ProductImage} key={imagePid} /> ); @@ -222,13 +227,15 @@ export default class Album extends Component {
{this.editAlbumDialog} {this.archiveAlbumDialog} - this.renderTitle(title, album.images.length)} - position={position} - albumsCount={albumsCount} - contentClassName={styles.albumContent} - onSort={this.handleMove} - actions={this.getAlbumActions()} + + this.renderTitle(title, album.images.length)} + position={position} + albumsCount={albumsCount} + contentClassName={styles.albumContent} + onSort={this.handleMove} + actions={this.getAlbumActions()} > {albumContent} diff --git a/ashes/src/components/images/edit-image.jsx b/ashes/src/components/images/edit-image.jsx index 273f76a089..6817e50e3a 100644 --- a/ashes/src/components/images/edit-image.jsx +++ b/ashes/src/components/images/edit-image.jsx @@ -29,6 +29,14 @@ class EditImage extends Component { alt: this.props.image.alt, }; + componentWillReceiveProps(nextProps: Props) { + this.setState({ + src: nextProps.image.src || '', + title: nextProps.image.title || '', + alt: nextProps.image.alt || '', + }); + } + get closeAction() { return ×; } diff --git a/ashes/src/components/images/image.jsx b/ashes/src/components/images/image.jsx index 340455c7e1..8e0610911c 100644 --- a/ashes/src/components/images/image.jsx +++ b/ashes/src/components/images/image.jsx @@ -22,15 +22,20 @@ export type Props = { image: ImageFile; editImage: (info: ImageInfo) => Promise<*>; deleteImage: () => Promise<*>; - imagePid: string|number; + imagePid: string | number; + imageComponent?: string; }; +type DefaultProps = { + imageComponent: string; +} + type State = { editMode: boolean; deleteMode: boolean; }; -export default class Image extends Component { +export default class Image extends Component { props: Props; state: State = { @@ -38,6 +43,10 @@ export default class Image extends Component { deleteMode: false, }; + static defaultProps: DefaultProps = { + imageComponent: 'img', + }; + @autobind handleEditImage(): void { this.setState({ editMode: true }); @@ -122,7 +131,7 @@ export default class Image extends Component { } render() { - const { image, imagePid } = this.props; + const { image, imagePid, imageComponent } = this.props; return (
@@ -136,6 +145,7 @@ export default class Image extends Component { actions={this.getImageActions()} loading={image.loading} key={`${imagePid}`} + imageComponent={imageComponent} />
); diff --git a/ashes/src/components/images/images.jsx b/ashes/src/components/images/images.jsx index 8093356dbe..bd7624bd42 100644 --- a/ashes/src/components/images/images.jsx +++ b/ashes/src/components/images/images.jsx @@ -10,7 +10,7 @@ import { autobind } from 'core-decorators'; import React, { Component, Element } from 'react'; // components -import WaitAnimation from '../common/wait-animation'; +import WaitAnimation from 'components/common/wait-animation'; import { AddButton } from 'components/core/button'; import EditAlbum from './edit-album'; import Album from './album'; @@ -80,13 +80,14 @@ class Images extends Component { const album = { name: '', images: [] }; return ( - ); } @@ -106,18 +107,19 @@ class Images extends Component {
{albums.map((album: TAlbum, i: number) => { return ( - ) => this.props.uploadImages(context, album.id, files)} - editImage={(idx: number, form: ImageInfo) => this.props.editImage(context, album.id, idx, form)} - deleteImage={(idx: number) => this.props.deleteImage(context, album.id, idx)} - editAlbum={(album: TAlbum) => this.props.editAlbum(context, album.id, album)} - moveAlbum={(position: number) => this.props.moveAlbum(context, entityId, album.id, position)} - archiveAlbum={(id: number) => this.props.archiveAlbum(context, id)} - position={i} - albumsCount={albums.length} - key={album.id} - fetchAlbums={() => this.props.fetchAlbums(context, entityId)} + ) => this.props.uploadImages(context, album.id, files)} + albumsCount={albums.length} + editImage={(idx: number, form: ImageInfo) => this.props.editImage(context, album.id, idx, form)} + deleteImage={(idx: number) => this.props.deleteImage(context, album.id, idx)} + editAlbum={(album: TAlbum) => this.props.editAlbum(context, album.id, album)} + moveAlbum={(position: number) => this.props.moveAlbum(context, entityId, album.id, position)} + archiveAlbum={(id: number) => this.props.archiveAlbum(context, id)} + fetchAlbums={() => this.props.fetchAlbums(context, entityId)} + position={i} + key={album.id} /> ); })} diff --git a/ashes/src/components/object-form/form-field.jsx b/ashes/src/components/object-form/form-field.jsx new file mode 100644 index 0000000000..aa9e5ae2c3 --- /dev/null +++ b/ashes/src/components/object-form/form-field.jsx @@ -0,0 +1,18 @@ +/* @flow */ + +import React from 'react'; +import { FormField } from 'components/forms'; + +// TODO: fix content type +export default function renderFormField(name: string, content: any, options: AttrOptions) { + return ( + + {content} + + ); +} diff --git a/ashes/src/components/object-form/object-form-inner.jsx b/ashes/src/components/object-form/object-form-inner.jsx index ff242d9582..a54db1471e 100644 --- a/ashes/src/components/object-form/object-form-inner.jsx +++ b/ashes/src/components/object-form/object-form-inner.jsx @@ -2,21 +2,16 @@ * @flow */ +// libs import React, { Component } from 'react'; import _ from 'lodash'; import { autobind } from 'core-decorators'; import classNames from 'classnames'; +import invariant from 'invariant'; import { stripTags } from 'lib/text-utils'; import { isDefined } from 'lib/utils'; -import { FormField, FormFieldError } from '../forms'; -import { SliderCheckbox } from '../checkbox/checkbox'; -import CurrencyInput from '../forms/currency-input'; -import CustomProperty from '../products/custom-property'; -import DatePicker from '../datepicker/datepicker'; -import RichTextEditor from '../rich-text-editor/rich-text-editor'; -import { Dropdown } from '../dropdown'; -import SwatchInput from '../forms/swatch-input'; +import renderers from './renderers'; import type { AttrSchema } from 'paragons/object'; @@ -28,103 +23,24 @@ type Props = { onChange: (attributes: Attributes) => void, schema?: Object, className?: string, + processAttr?: Function, }; type State = { - isAddingProperty: boolean, - errors: {[id:string]: any}, + errors: { [id: string]: any } }; -type AttrOptions = { - required: boolean, - label: string, - isDefined: (value: any) => boolean, - disabled?: boolean, -}; - -const inputClass = 'fc-object-form__field-value'; - function formatLabel(label: string): string { return _.snakeCase(label).split('_').reduce((res, val) => { return `${res} ${_.capitalize(val)}`; }); } -// TODO: fix content type -export function renderFormField(name: string, content: any, options: AttrOptions) { - return ( - - {content} - - ); -} - -function guessType(value: any): string { - const typeOf = typeof value; - switch (typeOf) { - case 'string': - case 'number': - case 'boolean': - return typeOf; - default: - return 'string'; - } -} - export default class ObjectFormInner extends Component { props: Props; - state: State = { isAddingProperty: false, errors: {} }; - - get addCustomProperty() { - if (this.props.canAddProperty) { - return ( -
- Custom Property - - - -
- ); - } - } - - get customPropertyForm() { - if (this.state.isAddingProperty) { - return ( - this.setState({ isAddingProperty: false })} - /> - ); - } - } - - @autobind - handleAddProperty() { - this.setState({ isAddingProperty: true }); - } - - @autobind - handleCreateProperty(property: { fieldLabel: string, propertyType: string }) { - const { fieldLabel, propertyType } = property; - const value = (() => { - switch(propertyType) { - case('date'): return new Date().toString(); - case('bool'): return false; - default: return ''; - } - })(); - this.setState({ - isAddingProperty: false - }, () => this.handleChange(fieldLabel, propertyType, value)); - } + state: State = { + errors: {}, + }; @autobind handleChange(name: string, type: string, value: any) { @@ -149,160 +65,6 @@ export default class ObjectFormInner extends Component { this.props.onChange(newAttributes); } - renderBoolean(name: string, value: boolean, options: AttrOptions) { - const onChange = () => this.handleChange(name, 'bool', !value); - const sliderCheckbox = ( - - ); - - return renderFormField(name, sliderCheckbox, options); - } - - renderBool(...args: Array) { - return this.renderBoolean(...args); - } - - renderElement(name: string, value: any, options: AttrOptions) { - return renderFormField(name, value, options); - } - - renderDate(name: string, value: string, options: AttrOptions) { - const dateValue = new Date(value); - const onChange = (v: Date) => this.handleChange(name, 'date', v.toISOString()); - const dateInput = ; - return renderFormField(name, dateInput, options); - } - - renderPrice(name: string, value: any, options: AttrOptions) { - const priceValue: string = _.get(value, 'value', ''); - const priceCurrency: string = _.get(value, 'currency', 'USD'); - const onChange = value => this.handleChange(name, 'price', { - currency: priceCurrency, - value: Number(value) - }); - const currencyInput = ( - - ); - - return renderFormField(name, currencyInput, options); - } - - renderRichText(name: string, value: any, options: AttrOptions) { - const onChange = v => this.handleChange(name, 'richText', v); - const error = _.get(this.state, ['errors', name]); - const classForContainer = classNames('fc-object-form__field', { - '_has-error': error != null, - }); - const nameVal = _.kebabCase(name); - - return ( -
- - {error && } -
- ); - } - - renderString(name: string, value: string = '', options: AttrOptions) { - const onChange = ({target}) => { - return this.handleChange(name, 'string', target.value); - }; - const stringInput = ( - - ); - - return renderFormField(name, stringInput, options); - } - - renderNumber(name: string, value: ?number = null, options: AttrOptions) { - const onChange = ({target}) => { - return this.handleChange(name, 'number', target.value == '' ? null : Number(target.value)); - }; - const stringInput = ( - - ); - - return renderFormField(name, stringInput, options); - } - - renderOptions(name: string, value: any, options: AttrOptions) { - const fieldOptions = this.props.fieldsOptions && this.props.fieldsOptions[name]; - if (!fieldOptions) throw new Error('You must define fieldOptions for options fields'); - - const onChange = v => this.handleChange(name, 'options', v); - const error = _.get(this.state, ['errors', name]); - - return ( -
-
{options.label}
- - {error && } -
- ); - } - - renderText(name: string, value: string = '', options: AttrOptions) { - const onChange = ({target}) => { - return this.handleChange(name, 'text', target.value); - }; - const textInput = ( -