From 6d1a0d959998dd8ec26e32e4df4c067f91afdbbc Mon Sep 17 00:00:00 2001 From: Maksym Mykhailenko Date: Mon, 10 Jun 2019 08:59:29 +0800 Subject: [PATCH 1/2] winning submission for challenge 30092665 - Topcoder Connect - Wizard - Skills question --- src/components/Select/Select.jsx | 9 +- src/components/Select/Select.scss | 7 ++ .../detail/components/Accordion/Accordion.jsx | 6 +- .../SkillsQuestion/CheckboxGroup.jsx | 76 +++++++++++ .../SkillsQuestion/CheckboxGroup.scss | 118 ++++++++++++++++++ .../SkillsQuestion/SkillsQuestion.jsx | 110 ++++++++++++++++ .../SkillsQuestion/SkillsQuestion.scss | 22 ++++ .../detail/components/SpecQuestions.jsx | 14 ++- 8 files changed, 356 insertions(+), 6 deletions(-) create mode 100644 src/projects/detail/components/SkillsQuestion/CheckboxGroup.jsx create mode 100644 src/projects/detail/components/SkillsQuestion/CheckboxGroup.scss create mode 100644 src/projects/detail/components/SkillsQuestion/SkillsQuestion.jsx create mode 100644 src/projects/detail/components/SkillsQuestion/SkillsQuestion.scss diff --git a/src/components/Select/Select.jsx b/src/components/Select/Select.jsx index 9b114966e..92f300a7e 100644 --- a/src/components/Select/Select.jsx +++ b/src/components/Select/Select.jsx @@ -16,6 +16,9 @@ const Select = (props) => { if (props.showDropdownIndicator) { containerclass = 'react-select-container' } + if (props.heightAuto) { + containerclass += ' height-auto' + } if (props.createOption) { return ( @@ -24,7 +27,6 @@ const Select = (props) => { createOptionPosition="first" className={containerclass} classNamePrefix="react-select" - noOptionsMessage={() => ('Type to search')} /> ) } else { @@ -34,10 +36,13 @@ const Select = (props) => { createOptionPosition="first" className={containerclass} classNamePrefix="react-select" - noOptionsMessage={() => ('Type to search')} /> ) } } +Select.defaultProps = { + noOptionsMessage: () => 'Type to search' +} + export default Select diff --git a/src/components/Select/Select.scss b/src/components/Select/Select.scss index 682b85c02..f165bb4d4 100644 --- a/src/components/Select/Select.scss +++ b/src/components/Select/Select.scss @@ -5,6 +5,13 @@ $reactselectcontentheight: 20px; @mixin reactselectstyles { + &.height-auto .react-select__control{ + height: auto; + & > div { + height: auto; + } + } + & .react-select__control:hover { border-color: $tc-gray-50; } diff --git a/src/projects/detail/components/Accordion/Accordion.jsx b/src/projects/detail/components/Accordion/Accordion.jsx index 29c1352dd..3c9dcbe67 100644 --- a/src/projects/detail/components/Accordion/Accordion.jsx +++ b/src/projects/detail/components/Accordion/Accordion.jsx @@ -22,7 +22,8 @@ const TYPE = { ADD_ONS: 'add-ons', TEXTINPUT: 'textinput', TEXTBOX: 'textbox', - NUMBERINPUT: 'numberinput' + NUMBERINPUT: 'numberinput', + SKILLS: 'skills' } /** @@ -33,7 +34,7 @@ const TYPE = { * @returns {Function} valueMapper */ const createValueMapper = (valuesMap) => (value) => ( - valuesMap[value] && (valuesMap[value].summaryLabel || valuesMap[value].label) + valuesMap[value] && (valuesMap[value].summaryLabel || valuesMap[value].label || valuesMap[value].title) ) class Accordion extends React.Component { @@ -113,6 +114,7 @@ class Accordion extends React.Component { case TYPE.CHECKBOX_GROUP: return value.map(mapValue).join(', ') case TYPE.RADIO_GROUP: return mapValue(value) case TYPE.ADD_ONS: return `${value.length} selected` + case TYPE.SKILLS: return value.map(mapValue).join(', ') default: return value } } diff --git a/src/projects/detail/components/SkillsQuestion/CheckboxGroup.jsx b/src/projects/detail/components/SkillsQuestion/CheckboxGroup.jsx new file mode 100644 index 000000000..18394f155 --- /dev/null +++ b/src/projects/detail/components/SkillsQuestion/CheckboxGroup.jsx @@ -0,0 +1,76 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import cn from 'classnames' +import './CheckboxGroup.scss' + +class CheckboxGroup extends Component { + + constructor(props) { + super(props) + this.changeValue = this.changeValue.bind(this) + } + + changeValue() { + const value = [] + this.props.options.forEach((option, key) => { + if (this['element-' + key].checked) { + value.push(option.value) + } + }) + this.props.setValue(value) + this.props.onChange(this.props.name, value) + } + + render() { + const { label, name = 'tc-checkbox-group', options, layout, wrapperClass, getValue, disabled } = this.props + const curValue = getValue() || [] + + const renderOption = (cb, key) => { + const checked = curValue.includes(cb.value) + const checkboxDisabled = cb.disabled || disabled + const rClass = cn('tc-checkbox-group-item', { disabled, selected: checked }) + const id = name+'-opt-'+key + const setRef = (c) => this['element-' + key] = c + return ( +
+
+ +
+ + { + cb.description && checked &&
{cb.description}
+ } +
+ ) + } + const chkGrpClass = cn('tc-checkbox-group', wrapperClass, { + horizontal: layout === 'horizontal', + vertical: layout === 'vertical' + }) + return ( +
+ +
{options.map(renderOption)}
+
+ ) + } +} + +CheckboxGroup.PropTypes = { + options: PropTypes.arrayOf(PropTypes.object).isRequired +} + +CheckboxGroup.defaultProps = { + onChange: () => {} +} + +export default CheckboxGroup diff --git a/src/projects/detail/components/SkillsQuestion/CheckboxGroup.scss b/src/projects/detail/components/SkillsQuestion/CheckboxGroup.scss new file mode 100644 index 000000000..9e72d1e8f --- /dev/null +++ b/src/projects/detail/components/SkillsQuestion/CheckboxGroup.scss @@ -0,0 +1,118 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.tc-checkbox-group { + &.horizontal { + .group-options { + flex-direction: row; + justify-content: space-between; + } + } + &.vertical { + .group-options { + flex-direction: column; + } + } + .group-label { + @include roboto; + font-size: 13px; + display: block; + margin: 10px auto; + color: $tc-gray-70; + } + .group-options { + display: flex; + flex-direction: column; + } +} + +.tc-checkbox-group-item { + margin: 0 0 10px 0; + background-color: $tc-gray-neutral-light; + padding: 10px; + border-radius: 4px; + display: block; + width: auto; + + &:first-child { + margin-top: 10px; + } + &.disabled { + > label { + color: $tc-gray-30; + cursor: default; + } + } + &.selected { + background-color: $tc-dark-blue-10; + } + .checkmark { + flex: 0 0 20px; + height: 20px; + width: 20px; + margin: 0; + padding: 0; + vertical-align: bottom; + position: relative; + display: inline-block; + > input { + display: none; + &:checked ~ label { + background: $tc-dark-blue-100; + border-color: $tc-dark-blue-100; + &:after { + opacity: 1; + border-color: $tc-gray-0; + } + } + } + > label { + font-weight: 400; + color: $tc-gray-100; + font-size: 12px; + cursor: pointer; + position: absolute; + display: inline-block; + width: 20px; + height: 20px; + top: 0; + left: 0; + border-radius: 2px; + box-shadow: none; + border: 1px solid $tc-gray-50; + background: $tc-gray-neutral-light; + transition: all .150s ease-in-out; + + &:after { + opacity: 0; + content: ''; + position: absolute; + width: 13px; + height: 7px; + background: transparent; + top: 4px; + left: 3px; + border: 3px solid $tc-dark-blue-100; + border-top: none; + border-right: none; + transform: rotate(-45deg); + transition: all .150s ease-in-out; + } + } + } + .item-description { + color: $tc-gray-70; + font-size: 13px; + margin-top: 10px; + line-height: 20px; + } + > label { + @include roboto; + color: $tc-gray-100; + margin-right: 0; + display: inline-block; + vertical-align: middle; + margin-left: 10px; + user-select: none; + cursor: pointer; + } +} diff --git a/src/projects/detail/components/SkillsQuestion/SkillsQuestion.jsx b/src/projects/detail/components/SkillsQuestion/SkillsQuestion.jsx new file mode 100644 index 000000000..a53d393d9 --- /dev/null +++ b/src/projects/detail/components/SkillsQuestion/SkillsQuestion.jsx @@ -0,0 +1,110 @@ +import React from 'react' +import _ from 'lodash' +import PropTypes from 'prop-types' +import { HOC as hoc } from 'formsy-react' +import CheckboxGroup from './CheckboxGroup' +import Select from '../../../../components/Select/Select' +import './SkillsQuestion.scss' + +class SkillsQuestion extends React.Component { + constructor(props) { + super(props) + this.handleChange = this.handleChange.bind(this) + } + + handleChange(val = []) { + const { setValue, onChange, name } = this.props + onChange(name, val) + setValue(val) + } + + componentDidUpdate(prevProps) { + const { skillsCategoriesField, currentProjectData, options, getValue, onChange, setValue, name } = this.props + const prevSelectedCategories = _.get(prevProps.currentProjectData, skillsCategoriesField, []) + const selectedCategories = _.get(currentProjectData, skillsCategoriesField, []) + + if (selectedCategories.length !== prevSelectedCategories.length) { + const currentValues = getValue() || [] + const prevAvailableOptions = options.filter(option => _.intersection(option.categories, prevSelectedCategories).length > 0) + const nextAvailableOptions = options.filter(option => _.intersection(option.categories, selectedCategories).length > 0) + const prevValues = currentValues.filter(skill => prevAvailableOptions.some(option => option.value === skill)) + const nextValues = currentValues.filter(skill => nextAvailableOptions.some(option => option.value === skill)) + + if (prevValues.length < nextValues.length) { + onChange(name, prevValues) + setValue(prevValues) + } else if (prevValues.length > nextValues.length) { + onChange(name, nextValues) + setValue(nextValues) + } + } + } + + render() { + const { + isFormDisabled, + isPristine, + isValid, + getErrorMessage, + validationError, + disabled, + currentProjectData, + skillsCategoriesField, + options, + getValue + } = this.props + + const selectedCategories = _.get(currentProjectData, skillsCategoriesField, []) + const availableOptions = options.filter(option => _.intersection(option.categories, selectedCategories).length > 0) + let currentValues = getValue() || [] + currentValues = currentValues.filter(skill => availableOptions.some(option => option.value === skill)) + + const questionDisabled = isFormDisabled() || disabled || selectedCategories.length === 0 + const hasError = !isPristine() && !isValid() + const errorMessage = getErrorMessage() || validationError + + const checkboxGroupOptions = availableOptions.filter(option => option.isFrequent) + const checkboxGroupValues = currentValues.filter(val => _.some(checkboxGroupOptions, option => option.value === val )) + const selectGroupOptions = availableOptions.filter(option => !option.isFrequent) + const selectGroupValues = currentValues.filter(val => _.some(selectGroupOptions, option => option.value === val )) + + return ( +
+ checkboxGroupValues} + setValue={(val) => { this.handleChange(_.union(val, selectGroupValues)) }} + /> +
+