diff --git a/package.json b/package.json
index d7e139a22..0fed87c4f 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"react-addons-css-transition-group": "^0.14.8",
"react-dom": "^0.14.8",
"react-dotdotdot": "^1.0.4",
+ "react-forms": "^2.0.0-beta33",
"react-layout-pane": "^0.1.16",
"react-redux": "^4.0.6",
"react-redux-form": "^0.14.3",
diff --git a/src/projects/create/components/AppProjectForm.jsx b/src/projects/create/components/AppProjectForm.jsx
new file mode 100644
index 000000000..e7b614383
--- /dev/null
+++ b/src/projects/create/components/AppProjectForm.jsx
@@ -0,0 +1,158 @@
+
+import React, { Component, PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { clearLoadedProject } from '../../../actions/project'
+import DevicesComponent from './Devices'
+import { Form, SubmitButton, TextInput, RadioGroupInput, TextareaInput, SliderRadioGroupInput, Validations } from 'appirio-tech-react-components'
+
+require('./CreateProject.scss')
+
+const appTypeOptions = [
+ {
+ value: 'ios',
+ label: 'iOS'
+ }, {
+ value: 'android',
+ label: 'Android'
+ }, {
+ value: 'web',
+ label: 'Web'
+ }, {
+ value: 'hybrid',
+ label: 'Hybrid'
+ }
+]
+
+const projectTypes = [
+ {
+ value: 'visual_design',
+ title: 'Visualize an app idea',
+ desc:
5-7 days, from $3,500
,
+ info: 'Wireframes, Visual Design'
+ }, {
+ value: 'visual_prototype',
+ title: 'Prototype an app',
+ desc: 14+ days, from $15,000
,
+ info: 'Visual or HTML prototype'
+ }, {
+ value: 'app_dev',
+ title: 'Fully develop an app',
+ desc: from $30,000
,
+ info: 'Design, Front End, Back End, Integration and API'
+ }
+]
+
+
+const initalFormValue = {
+ newProject: {
+ details: {
+ appType: 'ios',
+ devices: ['phone'],
+ utm: { code: ''}
+ },
+ type: 'visual_prototype'
+ }
+}
+
+class AppProjectForm extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ componentWillMount() {
+ this.props.clearLoadedProject()
+ }
+
+ // componentWillUpdate(nextProps) {
+ // if (!nextProps.isLoading &&
+ // nextProps.project.id) {
+ // console.log('project created', nextProps.project)
+ // this.props.router.push('/projects/' + nextProps.project.id )
+ // }
+ // }
+
+ render () {
+ return (
+
+ )
+ }
+}
+
+AppProjectForm.propTypes = {
+ submitHandler: PropTypes.func.isRequired
+}
+
+const mapStateToProps = ({ newProject, projectState }) => ({
+ newProject,
+ isLoading: projectState.isLoading,
+ project: projectState.project
+})
+
+const actionCreators = { clearLoadedProject}
+
+export default connect(mapStateToProps, actionCreators)(AppProjectForm)
diff --git a/src/projects/create/CreateProject.scss b/src/projects/create/components/CreateProject.scss
similarity index 95%
rename from src/projects/create/CreateProject.scss
rename to src/projects/create/components/CreateProject.scss
index 0172d906a..3596e2bef 100644
--- a/src/projects/create/CreateProject.scss
+++ b/src/projects/create/components/CreateProject.scss
@@ -76,7 +76,7 @@
position:absolute;
right: 20px;
top: 20px;
- // background: url('../images/x-mark-big.svg') no-repeat 0 0;
+ background: url('../../images/x-mark-big.svg') no-repeat 0 0;
width: 14px;
height: 14px;
background-size: 14px 14px;
@@ -219,35 +219,35 @@
}
&.phone{
span.icon{
- // background: url('../images/phone.svg') no-repeat 0 0;
+ background: url('../../images/phone.svg') no-repeat 0 0;
width: 33px;
background-size: 33px 50px;
}
}
&.tablet{
span.icon{
- // background: url('../images/tablet.svg') no-repeat 0 0;
+ background: url('../../images/tablet.svg') no-repeat 0 0;
width: 43px;
background-size: 43px 50px;
}
}
&.desktop{
span.icon{
- // background: url('../images/desktop.svg') no-repeat 0 0;
+ background: url('../../images/desktop.svg') no-repeat 0 0;
width: 55px;
background-size: 55px 50px;
}
}
&.apple-watch{
span.icon{
- // background: url('../images/watch-apple.svg') no-repeat 0 0;
+ background: url('../../images/watch-apple.svg') no-repeat 0 0;
width: 38px;
background-size: 38px 50px;
}
}
&.android-watch{
span.icon{
- // background: url('../images/watch-android.svg') no-repeat 0 0;
+ background: url('../../images/watch-android.svg') no-repeat 0 0;
width: 38px;
background-size: 38px 50px;
}
@@ -261,7 +261,7 @@
color: $tc-gray-40;
font-size:12px;
line-height: 155px;
- // background: url('../images/divider.png') no-repeat 50% 0;
+ background: url('../../images/divider.png') no-repeat 50% 0;
}
}
}
@@ -395,7 +395,7 @@
height: 16px;
border: none!important;
box-shadow: none!important;
- // background: url('../images/slider-bg.png') no-repeat 0 0;
+ background: url('../../images/slider-bg.png') no-repeat 0 0;
}
&::-webkit-slider-thumb {
-webkit-appearance: none;
@@ -413,7 +413,7 @@
&::-moz-range-track {
width: 600px;
height: 16px;
- // background: url('../images/slider-bg.png') no-repeat 0 0;
+ background: url('../../images/slider-bg.png') no-repeat 0 0;
border: none;
}
diff --git a/src/projects/create/CreateView.jsx b/src/projects/create/components/CreateView.jsx
similarity index 62%
rename from src/projects/create/CreateView.jsx
rename to src/projects/create/components/CreateView.jsx
index 5a05f3d10..5020f68c8 100644
--- a/src/projects/create/CreateView.jsx
+++ b/src/projects/create/components/CreateView.jsx
@@ -1,11 +1,12 @@
import _ from 'lodash'
import React, { Component, PropTypes } from 'react'
-import { ROLE_TOPCODER_MANAGER, ROLE_ADMINISTRATOR } from '../../config/constants'
-import WorkProjectForm from './WorkProjectForm'
+import { ROLE_TOPCODER_MANAGER, ROLE_ADMINISTRATOR } from '../../../config/constants'
import AppProjectForm from './AppProjectForm'
+import GenericProjectForm from './GenericProjectForm'
import { connect } from 'react-redux'
-import { createProject } from '../../actions/project'
+import { withRouter } from 'react-router'
+import { createProject } from '../../../actions/project'
require('./CreateProject.scss')
@@ -15,13 +16,21 @@ class CreateView extends Component {
super(props)
}
+ componentWillUpdate(nextProps) {
+ if (!nextProps.isLoading &&
+ nextProps.project.id) {
+ console.log('project created', nextProps.project)
+ this.props.router.push('/projects/' + nextProps.project.id )
+ }
+ }
+
handleSelect(index, last) {
console.log('SelectedTab: ' + index, ', LastTab: ' + last)
}
createProject(val) {
console.log('creating project', val)
- this.props.createProject(val)
+ createProject(val.newProject)
}
switchTab(val) {
@@ -48,12 +57,14 @@ class CreateView extends Component {
// Todo select based on Tab
form =
} else {
- form =
+ // form =
+ tabs = this.renderTabs()
+ form =
}
return (
@@ -72,5 +83,9 @@ CreateView.defaultProps = {
currentTab: 0
}
-const mapDispatchToProps = { createProject }
-export default connect(null, mapDispatchToProps)(CreateView)
+const mapStateToProps = ({projectState }) => ({
+ isLoading: projectState.isLoading,
+ project: projectState.project
+})
+const actionCreators = { createProject }
+export default withRouter(connect(mapStateToProps, actionCreators)(CreateView))
diff --git a/src/projects/create/components/Devices.jsx b/src/projects/create/components/Devices.jsx
new file mode 100644
index 000000000..bda056089
--- /dev/null
+++ b/src/projects/create/components/Devices.jsx
@@ -0,0 +1,137 @@
+
+import React, { PropTypes } from 'react'
+import { TiledCheckboxInput, BaseInputField } from 'appirio-tech-react-components'
+
+import _ from 'lodash'
+
+const devicesSet1 = [
+ {
+ title: 'Phone',
+ value: 'phone',
+ desc: 'iOS, Android, Hybrid'
+ }, {
+ title: 'Tablet',
+ value: 'tablet',
+ desc: 'iOS, Android, Hybrid'
+ }, {
+ title: 'Desktop',
+ value: 'desktop',
+ desc: 'All OS'
+ }
+]
+const devicesSet2 = [
+ {
+ title: 'Apple Watch',
+ value: 'apple-watch',
+ desc: 'Watch OS'
+ }, {
+ title: 'Android Watch',
+ value: 'android-watch',
+ desc: 'Android Wear'
+ }
+]
+
+const set1Values = _.map(devicesSet1, 'value')
+const set2Values = _.map(devicesSet2, 'value')
+
+class DevicesComponent extends BaseInputField {
+ constructor(props) {
+ super(props)
+ this.onChange = this.onChange.bind(this)
+ }
+
+ componentWillMount() {
+ const value = this.props.value || []
+ this.setState({
+ dirty: false,
+ valid: false,
+ value,
+ set1: _.intersection(set1Values, value),
+ set2: _.intersection(set2Values, value)
+ })
+ }
+ onChange(fieldName, newValue) {
+ const { onFieldChange, validateField, name, validations} = this.props
+
+ // determine the value that was just added
+ let justUpdated = _.difference(newValue, this.state.value)
+ if (!justUpdated.length) {
+ justUpdated = _.difference(this.state.value, newValue)
+ }
+ justUpdated = justUpdated[0]
+
+ const newDevices = this.updateDeviceList(this.state.value, justUpdated)
+ const results = validateField(newDevices, validations)
+ const isValid = results && !results.hasError || true
+ this.setState({
+ dirty: true,
+ value: newDevices,
+ set1: _.intersection(set1Values, newDevices),
+ set2: _.intersection(set2Values, newDevices),
+ valid: isValid,
+ errorMessage: _.get(results, 'errorMessage', '')
+ })
+ onFieldChange(name, newValue, isValid)
+ }
+
+ /*
+ * This is a wrapper function to update device list state.
+ * It uses model Actions from react-redux-form to dispatch the change
+ * action.
+ */
+ updateDeviceList(devices, val) {
+
+ // if val from set1 is selected values from set2 cannot be selected
+ // and vice-versa...
+ const reset =
+ (_.intersection(set1Values, devices).length
+ && _.indexOf(set2Values, val) > -1)
+ || (_.intersection(set2Values, devices).length
+ && _.indexOf(set1Values, val) > -1)
+
+ if (reset) {
+ return [val]
+ } else {
+ return _.xor(devices, [val])
+ }
+ }
+
+ render() {
+ return (
+
+
Pick target device(s)
+
+
+ )
+ }
+}
+DevicesComponent.displayName = 'DevicesInputField'
+DevicesComponent.defaultProps = _.merge({}, DevicesComponent.defaultProps, {value: []})
+
+DevicesComponent.propTypes = _.assign({}, DevicesComponent.propTypes, {
+ value: PropTypes.arrayOf(PropTypes.string.isRequired)
+})
+
+export default DevicesComponent
diff --git a/src/projects/create/components/GenericProjectForm.jsx b/src/projects/create/components/GenericProjectForm.jsx
new file mode 100644
index 000000000..e24217293
--- /dev/null
+++ b/src/projects/create/components/GenericProjectForm.jsx
@@ -0,0 +1,58 @@
+import React, { Component } from 'react'
+import { Form, TextInput, TextareaInput, SubmitButton, Validations } from 'appirio-tech-react-components'
+
+const initialValue = {
+ newProject: {
+ name: 'initial',
+ description: '',
+ type: 'generic'
+ }
+}
+class GenericProjectForm extends Component {
+
+ componentWillMount() {
+ this.setState(initialValue)
+ }
+
+ onSubmit(formValue) {
+ console.log(formValue)
+ }
+
+ onChange(formValue) {
+ this.setState(formValue)
+ console.log(formValue)
+ }
+
+ render() {
+ return (
+
+ )
+ }
+}
+
+export default GenericProjectForm
diff --git a/src/projects/create/AppProjectForm.jsx b/src/projects/create/components/old/AppProjectForm.jsx
similarity index 100%
rename from src/projects/create/AppProjectForm.jsx
rename to src/projects/create/components/old/AppProjectForm.jsx
diff --git a/src/projects/create/components/old/AppProjectReduxForm.jsx b/src/projects/create/components/old/AppProjectReduxForm.jsx
new file mode 100644
index 000000000..a5e8acb62
--- /dev/null
+++ b/src/projects/create/components/old/AppProjectReduxForm.jsx
@@ -0,0 +1,139 @@
+
+import React, { Component, PropTypes } from 'react'
+import { connect } from 'react-redux'
+import { Form } from 'react-redux-form'
+import { withRouter } from 'react-router'
+
+import { createProject, clearLoadedProject } from '../../../actions/project'
+import DevicesComponent from './Devices'
+import ProjectTypeSelector from './ProjectTypeSelector/ProjectType'
+import AppTypeSelector from './AppTypeSelector'
+import { InputFormField, TextareaFormField } from 'appirio-tech-react-components'
+
+require('./CreateProject.scss')
+
+
+class AppProjectForm extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ componentWillMount() {
+ this.props.clearLoadedProject()
+ }
+
+ componentWillUpdate(nextProps) {
+ if (!nextProps.isLoading &&
+ nextProps.project.id) {
+ console.log('project created', nextProps.project)
+ this.props.router.push('/projects/' + nextProps.project.id )
+ }
+ }
+
+ render () {
+
+ return (
+
+ )
+ }
+}
+
+AppProjectForm.propTypes = {
+ submitHandler: PropTypes.func.isRequired
+}
+
+const mapStateToProps = ({ newProject, projectState }) => ({
+ newProject,
+ isLoading: projectState.isLoading,
+ project: projectState.project
+})
+
+const actionCreators = { createProject, clearLoadedProject}
+
+export default withRouter(connect(mapStateToProps, actionCreators)(AppProjectForm))
diff --git a/src/projects/create/components/old/AppTypeSelector.jsx b/src/projects/create/components/old/AppTypeSelector.jsx
new file mode 100644
index 000000000..2c96d7707
--- /dev/null
+++ b/src/projects/create/components/old/AppTypeSelector.jsx
@@ -0,0 +1,57 @@
+
+import React, { Component, PropTypes } from 'react'
+import { Field } from 'react-redux-form'
+
+const typeOptions = [{
+ val: 'ios',
+ label: 'iOS'
+}, {
+ val: 'android',
+ label: 'Android'
+}, {
+ val: 'web',
+ label: 'Web'
+}, {
+ val: 'hybrid',
+ label: 'Hybrid'
+}]
+
+class AppTypeSelector extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ // creating a function to render each radio button
+ const typeFunc = (item, index) => {
+ // adding classes eg. "phone active"
+ const id = 'radio-option-' + index
+ return (
+
+
+ {item.label}
+
+ )
+ }
+ return (
+
+
App Type:
+
+ { typeOptions.map(typeFunc) }
+
+
+ )
+ }
+}
+
+
+AppTypeSelector.propTypes = {
+ modelName: PropTypes.string.isRequired
+}
+
+export default AppTypeSelector
diff --git a/src/projects/create/components/old/Devices.jsx b/src/projects/create/components/old/Devices.jsx
new file mode 100644
index 000000000..aca5af1db
--- /dev/null
+++ b/src/projects/create/components/old/Devices.jsx
@@ -0,0 +1,122 @@
+
+import React, { Component, PropTypes } from 'react'
+import { actions as modelActions } from 'react-redux-form'
+import { connect } from 'react-redux'
+import _ from 'lodash'
+import classNames from 'classnames'
+
+const devicesSet1 = [
+ {
+ title: 'Phone',
+ val: 'phone',
+ desc: 'iOS, Android, Hybrid'
+ }, {
+ title: 'Tablet',
+ val: 'tablet',
+ desc: 'iOS, Android, Hybrid'
+ }, {
+ title: 'Desktop',
+ val: 'desktop',
+ desc: 'All OS'
+ }
+]
+const devicesSet2 = [
+ {
+ title: 'Apple Watch',
+ val: 'apple-watch',
+ desc: 'Watch OS'
+ }, {
+ title: 'Android Watch',
+ val: 'android-watch',
+ desc: 'Android Wear'
+ }
+]
+
+class DevicesComponent extends Component {
+ constructor(props) {
+ super(props)
+ }
+
+ render() {
+ const { devices } = this.props
+ // creating a function to render each device item
+ const deviceFunc = (item, index) => {
+ // adding classes eg. "phone active"
+ const itemClassnames = classNames(
+ item.val, {
+ active: _.indexOf(devices, item.val) > -1
+ }
+ )
+ const handleClick = this.props.toggleDevice.bind(item.val)
+ return (
+
+
+ {item.title}
+ {item.desc}
+
+ )
+ }
+ return (
+
+
Pick target device(s)
+
+ { devicesSet1.map(deviceFunc) }
+
+ Or
+
+ { devicesSet2.map(deviceFunc) }
+
+
+ )
+ }
+}
+
+/*
+ * This is a wrapper function to update device list state.
+ * It uses model Actions from react-redux-form to dispatch the change
+ * action.
+ */
+function updateDeviceList(devicesBeforeAction, val) {
+ return (dispatch) => {
+ const modelName = 'newProject.details.devices'
+ const set1 = _.map(devicesSet1, 'val')
+ const set2 = _.map(devicesSet2, 'val')
+
+ // if val from set1 is selected values from set2 cannot be selected
+ // and vice-versa...
+ const reset =
+ (_.intersection(set1, devicesBeforeAction).length
+ && _.indexOf(set2, val) > -1)
+ || (_.intersection(set2, devicesBeforeAction).length
+ && _.indexOf(set1, val) > -1)
+
+ if (reset) {
+ dispatch(modelActions.change(modelName, [val]))
+ } else {
+ dispatch(modelActions.xor(modelName, val))
+ }
+ }
+}
+
+DevicesComponent.propTypes = {
+ devices: PropTypes.arrayOf(PropTypes.string).isRequired
+}
+const actionCreators = { updateDeviceList }
+const mapStateToProps = ({newProject}) => ({
+ devices: newProject.details.devices
+})
+
+// Merging props so that we can use determine the current
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const props = Object.assign({}, ownProps, stateProps, dispatchProps, {
+ toggleDevice: (val) => {
+ props.updateDeviceList(stateProps.devices, val)
+ }
+ })
+ return props
+}
+
+export default connect(mapStateToProps, actionCreators, mergeProps)(DevicesComponent)
diff --git a/src/projects/create/components/old/ProjectTypeSelector/ProjectType.jsx b/src/projects/create/components/old/ProjectTypeSelector/ProjectType.jsx
new file mode 100644
index 000000000..7b30fed07
--- /dev/null
+++ b/src/projects/create/components/old/ProjectTypeSelector/ProjectType.jsx
@@ -0,0 +1,138 @@
+
+import React, { Component, PropTypes } from 'react'
+import { actions as modelActions } from 'react-redux-form'
+import { connect } from 'react-redux'
+import _ from 'lodash'
+import classNames from 'classnames'
+
+const projectTypes = [{
+ val: 'visual_design',
+ numericVal: 0,
+ title: 'Visualize an app idea',
+ desc: 5-7 days, from $3,500
,
+ info: 'Wireframes, Visual Design'
+}, {
+ val: 'visual_prototype',
+ numericVal: 1,
+ title: 'Prototype an app',
+ desc: 14+ days, from $15,000
,
+ info: 'Visual or HTML prototype'
+}, {
+ val: 'app_dev',
+ numericVal: 2,
+ title: 'Fully develop an app',
+ desc: from $30,000
,
+ info: 'Design, Front End, Back End, Integration and API'
+}]
+
+const getTypeIndex = (val) => _.findIndex(projectTypes, (t) => t.val === val)
+
+class ProjectTypeSelector extends Component {
+ constructor(props) {
+ super(props)
+ this.onSliderChange = this.props.onSliderChange.bind(this)
+ }
+
+ render() {
+ const { type } = this.props
+ // creating a function to render each type title + desc
+ const itemFunc = (item, index) => {
+ // handle active class
+ const itemClassnames = classNames( 'selector', {
+ active: type === item.val
+ })
+ const idx = getTypeIndex(item.val)
+ const handleClick = this.props.onSliderChange.bind(this, idx)
+ return (
+
+
{item.title}
+ {item.desc}
+
+ )
+ }
+
+ // function to render item info
+ const itemInfoFunc = (item, index) => {
+ // handle active class
+ const itemClassnames = classNames({active: type === item.val})
+ const idx = getTypeIndex(item.val)
+ const handleClick = this.props.onSliderChange.bind(this, idx)
+ return (
+
+
+ )
+ }
+
+ return (
+ /**
+ * TODO Using onInput trigger instead of onChange.
+ * onChange is showing some funky behavior at least in Chrome.
+ * This functionality should be tested in other browsers
+ */
+
+
What would you like to do?
+
+ {projectTypes.map(itemFunc)}
+
+
+
+
+
+ {projectTypes.map(itemInfoFunc)}
+
+
+ )
+ }
+}
+
+
+ProjectTypeSelector.propTypes = {
+ type: PropTypes.string.isRequired,
+ mappedProjectType: PropTypes.number.isRequired
+}
+const actionCreators = {
+ sliderChangeCreator: (modelName, val) => {
+ return (dispatch) => {
+ // get val from index
+ dispatch(modelActions.change(modelName, projectTypes[val].val))
+ }
+ }
+}
+
+const mapStateToProps = ({newProject}) => ({
+ type: newProject.type,
+ mappedProjectType: _.findIndex(projectTypes, (t) => newProject.type === t.val)
+})
+
+// Merging props so that we can use determine the current
+const mergeProps = (stateProps, dispatchProps, ownProps) => {
+ const props = Object.assign({}, ownProps, stateProps, dispatchProps, {
+ onSliderChange: (event) => {
+ // handling case where event is fired by clicking on range or div.
+ // the latter provides the index, the former an event
+ const idx = (typeof event !== 'number') ? parseInt(event.target.value) : event
+ props.sliderChangeCreator('newProject.type', idx)
+ }
+ })
+ return props
+}
+
+export default connect(mapStateToProps, actionCreators, mergeProps)(ProjectTypeSelector)
diff --git a/src/projects/create/WorkProjectForm.jsx b/src/projects/create/components/old/WorkProjectForm.jsx
similarity index 86%
rename from src/projects/create/WorkProjectForm.jsx
rename to src/projects/create/components/old/WorkProjectForm.jsx
index 873f5360c..0a72dbb9e 100644
--- a/src/projects/create/WorkProjectForm.jsx
+++ b/src/projects/create/components/old/WorkProjectForm.jsx
@@ -3,9 +3,9 @@
import React, { Component, PropTypes } from 'react'
import { connect } from 'react-redux'
import { Form, actions as modelActions} from 'react-redux-form'
-import { clearLoadedProject } from '../../actions/project'
+import { clearLoadedProject } from '../../../actions/project'
import { withRouter } from 'react-router'
-import { InputFormField, TextareaFormField } from 'appirio-tech-react-components'
+import { TextInputField, InputFormField, TextareaFormField } from 'appirio-tech-react-components'
class WorkProjectForm extends Component {
@@ -31,6 +31,14 @@ class WorkProjectForm extends Component {