diff --git a/src/assets/images/select-arrow.svg b/src/assets/images/select-arrow.svg new file mode 100644 index 0000000000..5299254ccf --- /dev/null +++ b/src/assets/images/select-arrow.svg @@ -0,0 +1,23 @@ + + + + Shape + Created with Sketch. + + + + + + + + + + + + + + + + + + diff --git a/src/assets/images/tools/device-types/console.svg b/src/assets/images/tools/device-types/console.svg old mode 100755 new mode 100644 index e5c3c983f5..237abfc974 --- a/src/assets/images/tools/device-types/console.svg +++ b/src/assets/images/tools/device-types/console.svg @@ -1,8 +1 @@ - - - - - - - - +004-consoleCreated with Sketch. diff --git a/src/assets/images/tools/device-types/desktop.svg b/src/assets/images/tools/device-types/desktop.svg old mode 100755 new mode 100644 index b8d19c0692..cc371322ff --- a/src/assets/images/tools/device-types/desktop.svg +++ b/src/assets/images/tools/device-types/desktop.svg @@ -1,7 +1 @@ - - - - - - - +computerCreated with Sketch. diff --git a/src/assets/images/tools/device-types/laptop.svg b/src/assets/images/tools/device-types/laptop.svg old mode 100755 new mode 100644 index a5903620ff..d54366f39f --- a/src/assets/images/tools/device-types/laptop.svg +++ b/src/assets/images/tools/device-types/laptop.svg @@ -1,8 +1 @@ - - - - - - - - +notebookCreated with Sketch. diff --git a/src/assets/images/tools/device-types/smartphone.svg b/src/assets/images/tools/device-types/smartphone.svg old mode 100755 new mode 100644 index 7786f28c58..8ed43adf1a --- a/src/assets/images/tools/device-types/smartphone.svg +++ b/src/assets/images/tools/device-types/smartphone.svg @@ -1,9 +1 @@ - - - - - - - - - +002-mobile-phone-variantCreated with Sketch. diff --git a/src/assets/images/tools/device-types/tablet.svg b/src/assets/images/tools/device-types/tablet.svg old mode 100755 new mode 100644 index 3f4ed7a2fa..fa1a0d6519 --- a/src/assets/images/tools/device-types/tablet.svg +++ b/src/assets/images/tools/device-types/tablet.svg @@ -1,7 +1 @@ - - - - - - - + diff --git a/src/assets/images/tools/device-types/wearable.svg b/src/assets/images/tools/device-types/wearable.svg index ad61fc178c..646ae68544 100644 --- a/src/assets/images/tools/device-types/wearable.svg +++ b/src/assets/images/tools/device-types/wearable.svg @@ -1,8 +1 @@ - - - - - - - - +005-smart-watchCreated with Sketch. diff --git a/src/shared/components/InputSelect/MenuItem/index.jsx b/src/shared/components/InputSelect/MenuItem/index.jsx new file mode 100644 index 0000000000..d834262b46 --- /dev/null +++ b/src/shared/components/InputSelect/MenuItem/index.jsx @@ -0,0 +1,44 @@ +import React from 'react'; +import PT from 'prop-types'; +import cn from 'classnames'; +import _ from 'lodash'; +import './style.scss'; + +export default function MenuItem({ + value, data, labelKey, onClick, +}) { + let isSelect = false; + if (value === data[labelKey]) { + isSelect = true; + } + + return ( +
{ + onClick(data); + }} + onClick={() => { + onClick(data); + }} + > + {data[labelKey]} +
+ ); +} + +MenuItem.defaultProps = { + data: {}, + labelKey: '', + onClick: _.noop, + value: '', +}; + +MenuItem.propTypes = { + data: PT.shape(), + labelKey: PT.string, + onClick: PT.func, + value: PT.string, +}; diff --git a/src/shared/components/InputSelect/MenuItem/style.scss b/src/shared/components/InputSelect/MenuItem/style.scss new file mode 100644 index 0000000000..aec3220288 --- /dev/null +++ b/src/shared/components/InputSelect/MenuItem/style.scss @@ -0,0 +1,21 @@ +@import '~styles/mixins'; + +.item { + height: 40px; + line-height: 40px; + padding-left: 15px; + color: #262628; + font-size: 14px; + + &:focus { + outline: 0; + } + + &:hover { + background-color: $tc-dark-blue-10; + } +} + +.is-select { + @include roboto-bold; +} diff --git a/src/shared/components/InputSelect/index.jsx b/src/shared/components/InputSelect/index.jsx new file mode 100644 index 0000000000..a91c088093 --- /dev/null +++ b/src/shared/components/InputSelect/index.jsx @@ -0,0 +1,211 @@ +import React, { Component } from 'react'; +import { map, filter } from 'lodash'; +import DownArrowIcon from 'assets/images/select-arrow.svg'; +import uuid from 'uuid'; +import PT from 'prop-types'; +import cn from 'classnames'; +import MenuItem from './MenuItem'; +import './style.scss'; + + +export default class InputSelect extends Component { + constructor(props) { + super(props); + this.state = { + filterVal: '', + isShowModal: false, + _id: uuid(), + }; + + this.handleClickOutside = this.handleClickOutside.bind(this); + this.onToggleModal = this.onToggleModal.bind(this); + this.onSelect = this.onSelect.bind(this); + this.onFilterChange = this.onFilterChange.bind(this); + this.onLoadMore = this.onLoadMore.bind(this); + } + + componentDidMount() { + document.addEventListener('click', this.handleClickOutside); + } + + componentWillUnmount() { + document.removeEventListener('click', this.handleClickOutside); + } + + onToggleModal(evt) { + const { + disabled, + } = this.props; + + const { + isShowModal, + } = this.state; + + evt.stopPropagation(); + + if (disabled) { return; } + + this.setState({ + isShowModal: !isShowModal, + filterVal: '', + }); + } + + onReset() { + this.setState({ + isShowModal: false, + filterVal: '', + }); + } + + + onSelect(v) { + const { + onChange, + valueKey, + value, + } = this.props; + + const newVal = valueKey ? v[valueKey] : v.value; + + if (value !== newVal) { + onChange(v[valueKey]); + } + + this.setState({ + isShowModal: false, + filterVal: '', + }); + } + + onFilterChange(evt) { + evt.stopPropagation(); + this.setState({ + filterVal: evt.target.value, + }); + } + + onLoadMore(e) { + const { + hasMore, + isLoading, + onLoadMore, + } = this.props; + const { + filterVal, + } = this.state; + const element = e.target; + if (!hasMore || isLoading || filterVal.length) { return; } + + if (element.scrollHeight - element.scrollTop - element.clientHeight <= 10) { + onLoadMore(); + } + } + + handleClickOutside(e) { + const { + _id, + isShowModal, + } = this.state; + if (!isShowModal) { return false; } + let i = 0; + let node = e.target; + const REG = new RegExp(_id); + while (node && i < 5) { + if (REG.test(node.className)) { + return true; + } + i += 1; + node = node.parentNode; + } + + this.setState({ + isShowModal: false, + }); + return false; + } + + render() { + const { + value, + placeholder, + labelKey, + options, + } = this.props; + + const { + _id, + isShowModal, + filterVal, + } = this.state; + + let fiterList = options; + if (filterVal) { + const REG = new RegExp(filterVal, 'i'); + fiterList = filter(options, o => REG.test(o[labelKey])); + } + const list = map(fiterList, o => ( + + )); + + return ( +
+
{}} + onClick={this.onToggleModal} + > +
{value.length ? value : placeholder }
+
+
+ {isShowModal && options.length > 0 + ? ( +
+
+ + +
+
+ {list} +
+
+ ) : null} +
+ ); + } +} + + +InputSelect.defaultProps = { + options: [], + valueKey: '', + labelKey: '', + value: '', + placeholder: '', + disabled: false, + hasMore: false, + isLoading: false, + onChange: () => {}, + onLoadMore: () => {}, +}; + +InputSelect.propTypes = { + options: PT.arrayOf(PT.object), + valueKey: PT.string, + labelKey: PT.string, + value: PT.string, + placeholder: PT.string, + onChange: PT.func, + onLoadMore: PT.func, + hasMore: PT.bool, + isLoading: PT.bool, + disabled: PT.bool, +}; diff --git a/src/shared/components/InputSelect/style.scss b/src/shared/components/InputSelect/style.scss new file mode 100644 index 0000000000..0706cb0aff --- /dev/null +++ b/src/shared/components/InputSelect/style.scss @@ -0,0 +1,71 @@ +@import '~styles/mixins'; + +.container { + @include roboto-regular; + + position: relative; +} + +.input-container { + height: 40px; + display: flex; + align-items: center; + border-radius: 4px; + border: solid 1px #c3c3c8; + background-color: $tc-white; + + &:focus { + outline: 0; + } +} + +.input-label { + @include roboto-regular; + + flex: 1; + margin-left: 10px; + color: #262628; + font-size: 15px; + line-height: 1.33; +} + +.input-placeholder { + color: $tc-gray-30; +} + +.input-arrow { + width: 20px; + margin-top: -3px; +} + +.modal { + position: absolute; + z-index: 2; + width: 100%; + display: flex; + flex-direction: column; + border-radius: 5px; + box-shadow: 0 5px 35px 5px rgba(21, 21, 22, 0.1), 0 10px 14px -4px rgba(21, 21, 22, 0.3); + background-color: #fff; +} + +.modal-input-container { + display: flex; + height: 63px; + padding: 14px 17px; + align-items: center; + border-bottom: 1px solid $tc-gray-neutral-dark; + + input { + margin-bottom: 0 !important; + box-sizing: border-box; + border: 1px solid $tc-dark-blue-70 !important; + } +} + +.modal-list-container { + padding-top: 10px; + padding-bottom: 6px; + overflow-y: scroll; + max-height: 216px; +} diff --git a/src/shared/components/Settings/Tools/Devices/List/Item/index.jsx b/src/shared/components/Settings/Tools/Devices/List/Item/index.jsx index cc36dc7152..c550dc7be7 100644 --- a/src/shared/components/Settings/Tools/Devices/List/Item/index.jsx +++ b/src/shared/components/Settings/Tools/Devices/List/Item/index.jsx @@ -5,6 +5,7 @@ import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; import ReactSVG from 'react-svg'; +import cn from 'classnames'; import { isomorphy } from 'topcoder-react-utils'; import './styles.scss'; @@ -18,40 +19,30 @@ export default function Item(props) { const { device, index, + isEditing, onDeleteItem, onEditItem, } = props; - const hasSecondLine = () => { - if (_.isEmpty(device.operatingSystem) && _.isEmpty(device.osVersion) - && _.isEmpty(device.osLanguage)) { - return false; - } - - return true; - }; + const hasModel = !_.isEmpty(device.model); + const secondLine = device.deviceType + (device.manufacturer ? ` | ${device.manufacturer}` : '') + + (device.operatingSystem ? ` | ${device.operatingSystem}` : ''); return ( -
+
{ assets && assets.keys().includes(`./${device.deviceType.toLowerCase()}.svg`) ? : '' }
-
-
- { - `${_.isEmpty(device.manufacturer) ? '' : `${device.manufacturer} | `}${_.isEmpty(device.model) ? '' : `${device.model} | `}${device.deviceType}` - } +
+ {hasModel ? ( +
+ {device.model} +
+ ) : null} +
+ {secondLine}
- { - hasSecondLine() && ( -
- { - `${_.isEmpty(device.operatingSystem) ? '' : `${device.operatingSystem} `}${_.isEmpty(device.osVersion) ? '' : `${device.osVersion} `}${_.isEmpty(device.osLanguage) ? '' : `${device.osLanguage}`}` - } -
- ) - }
@@ -87,6 +78,7 @@ export default function Item(props) { Item.propTypes = { device: PT.shape().isRequired, index: PT.number.isRequired, + isEditing: PT.bool.isRequired, onDeleteItem: PT.func.isRequired, onEditItem: PT.func.isRequired, }; diff --git a/src/shared/components/Settings/Tools/Devices/List/Item/styles.scss b/src/shared/components/Settings/Tools/Devices/List/Item/styles.scss index 674c5f021b..5a283b9eec 100644 --- a/src/shared/components/Settings/Tools/Devices/List/Item/styles.scss +++ b/src/shared/components/Settings/Tools/Devices/List/Item/styles.scss @@ -8,10 +8,14 @@ justify-content: space-between; height: 100%; width: 100%; - padding: 20px; + padding: 19px 20px 18px 45px; border: 1px solid $tc-gray-10; border-bottom: none; background-color: $tc-white; + + &.isEditing { + border: 1px solid $tc-gray-75; + } } .device-info { @@ -24,7 +28,10 @@ .device-icon { height: 49px; width: 49px; - margin-right: 18px; + margin-right: 40px; + display: flex; + align-items: center; + justify-content: center; } .device-parameters { @@ -33,8 +40,7 @@ display: flex; flex-direction: column; justify-items: center; - justify-content: left; - font-size: 15px; + justify-content: center; line-height: 20px; padding-right: 20px; flex: 1; @@ -43,21 +49,26 @@ flex-direction: row; align-items: center; } -} -.parameter-first-line { - font-weight: 500; - color: $tc-black; - margin-bottom: 10px; + > div:first-child { + @include roboto-medium; - &.single-line { - margin-bottom: 0; + font-size: 15px !important; + letter-spacing: -0.16px !important; + color: $tc-black !important; } } +.parameter-model { + margin-bottom: 8px; +} + .parameter-second-line { - color: $tc-gray-50; - font-weight: 400; + @include roboto-regular; + + font-size: 13px; + letter-spacing: -0.14px; + color: #888894; } .operation-container { diff --git a/src/shared/components/Settings/Tools/Devices/List/index.jsx b/src/shared/components/Settings/Tools/Devices/List/index.jsx index dfa7877f91..5c934f3d65 100644 --- a/src/shared/components/Settings/Tools/Devices/List/index.jsx +++ b/src/shared/components/Settings/Tools/Devices/List/index.jsx @@ -13,6 +13,7 @@ export default function DeviceList(props) { onDeleteItem, disabled, onEditItem, + indexNo, } = props; return ( @@ -24,6 +25,7 @@ export default function DeviceList(props) { ? (
  • 0) { const newDeviceTrait = _.cloneDeep(deviceTrait); if (isEdit) { @@ -209,8 +211,6 @@ export default class Devices extends ConsentComponent { manufacturer: '', model: '', operatingSystem: '', - osVersion: '', - osLanguage: '', }; this.setState({ newDevice: empty, @@ -218,6 +218,7 @@ export default class Devices extends ConsentComponent { indexNo: null, isSubmit: false, }); + clearDeviceState(); // save personalization if (_.isEmpty(personalizationTrait)) { const personalizationData = { userConsent: answer }; @@ -250,30 +251,103 @@ export default class Devices extends ConsentComponent { } /** - * Update input value + * Update deviceType + * @param e event + */ + onUpdateType(val) { + const { + getManufacturers, + } = this.props; + + const newDevice = { deviceType: val }; + newDevice.manufacturer = ''; + newDevice.model = ''; + newDevice.operatingSystem = ''; + this.setState({ newDevice, isSubmit: false }); + + // preload manufacturers + getManufacturers(val); + } + + /** + * Update manufacturer * @param e event */ - onUpdateInput(e) { + onUpdateManufacturer(val) { + const { + getModels, + } = this.props; const { newDevice: device } = this.state; - const newDevice = { ...device }; - newDevice[e.target.name] = e.target.value; + const newDevice = { ..._.pick(device, ['deviceType']) }; + newDevice.manufacturer = val; + newDevice.model = ''; + newDevice.operatingSystem = ''; this.setState({ newDevice, isSubmit: false }); + // preload models + getModels(1, newDevice.deviceType, newDevice.manufacturer); } /** - * Update select value - * @param option selected value + * Update model + * @param e event */ - onUpdateSelect(option) { - if (option) { - const { newDevice: device } = this.state; - const newDevice = { ...device }; - newDevice[option.key] = option.name; - this.setState({ - newDevice, - isSubmit: false, - }); - } + onUpdateModel(val) { + const { + getOses, + // lookupData: {modelPage} + } = this.props; + const { newDevice: device } = this.state; + const newDevice = { ..._.pick(device, ['deviceType', 'manufacturer']) }; + newDevice.model = val; + newDevice.operatingSystem = ''; + this.setState({ newDevice, isSubmit: false }); + + // preload oses + getOses(1, newDevice.deviceType, newDevice.manufacturer, newDevice.model); + } + + /** + * Update operatingSystem + * @param e event + */ + onUpdateOs(val) { + const { newDevice: device } = this.state; + const newDevice = { ..._.pick(device, ['deviceType', 'manufacturer', 'model']) }; + newDevice.operatingSystem = val; + this.setState({ newDevice, isSubmit: false }); + } + + onLoadMoreModels() { + const { + lookupData: { + modelPage, + }, + getMoreModels, + } = this.props; + + const { + newDevice: { + deviceType, + manufacturer, + }, + } = this.state; + getMoreModels(modelPage + 1, deviceType, manufacturer); + } + + onLoadMoreOses() { + const { + lookupData: { + osPage, + }, + getMoreOses, + } = this.props; + const { + newDevice: { + deviceType, + manufacturer, model, + }, + } = this.state; + getMoreOses(osPage + 1, deviceType, manufacturer, model); } /** @@ -307,6 +381,7 @@ export default class Devices extends ConsentComponent { } onCancelEditStatus() { + const { clearDeviceState } = this.props; const { isEdit } = this.state; if (isEdit) { this.setState({ @@ -319,14 +394,26 @@ export default class Devices extends ConsentComponent { manufacturer: '', model: '', operatingSystem: '', - osVersion: '', - osLanguage: '', }, }); + clearDeviceState(); } } render() { + const { + lookupData, + } = this.props; + const { + types, + manufacturers, + models, + oses, + hasMoreModels, + hasMoreOses, + isModelsLoading, + isOsesLoading, + } = lookupData; const { deviceTrait, isMobileView, showConfirmation, indexNo, isEdit, formInvalid, isSubmit, @@ -360,6 +447,7 @@ export default class Devices extends ConsentComponent { @@ -382,15 +470,14 @@ export default class Devices extends ConsentComponent {
  • * Required - +
    @@ -419,40 +515,42 @@ export default class Devices extends ConsentComponent {
    - +
    -
    -
    - -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    - +
    @@ -481,6 +579,14 @@ export default class Devices extends ConsentComponent { ) }
    +
    +
    + Don't see your device? +
    +
    + Contact Support at support@topcoder.com +
    +
    @@ -499,15 +605,14 @@ export default class Devices extends ConsentComponent { * Required - - +
    @@ -530,30 +644,38 @@ export default class Devices extends ConsentComponent { Model - +
    - -
    - -
    -
    - - -
    -
    - - +
    @@ -582,18 +704,28 @@ export default class Devices extends ConsentComponent { ) } +
    +
    + Don't see your device? +
    +
    + Contact Support at support@topcoder.com +
    +
    { isMobileView && ( ) } + ); } diff --git a/src/shared/components/Settings/Tools/Devices/styles.scss b/src/shared/components/Settings/Tools/Devices/styles.scss index e8e5329c18..1ae894a088 100644 --- a/src/shared/components/Settings/Tools/Devices/styles.scss +++ b/src/shared/components/Settings/Tools/Devices/styles.scss @@ -190,6 +190,7 @@ display: flex; justify-content: center; align-items: center; + margin-top: 50px; .button-save, .button-cancel { @@ -266,3 +267,21 @@ .devices-container .form-container .row .field :global .Select-placeholder { color: $tc-gray-50; } + +.help-text-container { + margin-top: 45px; + margin-bottom: 40px; + font-size: 15px; + color: $tc-black; + text-align: center; +} + +.help-text-label { + @include roboto-bold; +} + +.help-text-email { + @include roboto-regular; + + margin-top: 10px; +} diff --git a/src/shared/containers/Settings.jsx b/src/shared/containers/Settings.jsx index dbe2b81913..75731d4247 100644 --- a/src/shared/containers/Settings.jsx +++ b/src/shared/containers/Settings.jsx @@ -160,6 +160,9 @@ function mapDispatchToProps(dispatch) { dispatch(profileActions.getLinkedAccountsDone(profile, tokenV3)); dispatch(profileActions.getExternalLinksDone(handle)); dispatch(profileActions.getCredentialDone(profile, tokenV3)); + } else if (settingsTab === TABS.TOOLS) { + dispatch(lookupActions.getTypesInit()); + dispatch(lookupActions.getTypesDone()); } }; @@ -173,6 +176,34 @@ function mapDispatchToProps(dispatch) { dispatch(profileActions.addWebLinkInit()); dispatch(profileActions.addWebLinkDone(handle, tokenV3, webLink)); }, + getDeviceTypes: () => { + dispatch(lookupActions.getTypesInit()); + dispatch(lookupActions.getTypesDone()); + }, + clearDeviceState: () => { + // reset manufacturers, models, oses + dispatch(lookupActions.getManufacturersInit()); + }, + getManufacturers: (type) => { + dispatch(lookupActions.getManufacturersInit()); + dispatch(lookupActions.getManufacturersDone(type)); + }, + getModels: (page, type, manufacturer) => { + dispatch(lookupActions.getModelsInit(page)); + dispatch(lookupActions.getModelsDone(page, type, manufacturer)); + }, + getMoreModels: (page, type, manufacturer) => { + dispatch(lookupActions.getModelsInit(page)); + dispatch(lookupActions.getModelsDone(page, type, manufacturer)); + }, + getOses: (page, type, manufacturer, model) => { + dispatch(lookupActions.getOsesInit(page)); + dispatch(lookupActions.getOsesDone(page, type, manufacturer, model)); + }, + getMoreOses: (page, type, manufacturer, model) => { + dispatch(lookupActions.getOsesInit(page)); + dispatch(lookupActions.getOsesDone(page, type, manufacturer, model)); + }, deleteWebLink: (handle, tokenV3, webLink) => { dispatch(profileActions.deleteWebLinkInit(webLink)); dispatch(profileActions.deleteWebLinkDone(handle, tokenV3, webLink));