From e3df97bef37a51e773d8989c2c8679ebe7c2cad7 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 23 Sep 2020 10:53:27 +0300 Subject: [PATCH 01/25] Gig apply page init --- .../Datepicker/__snapshots__/index.jsx.snap | 4 +- .../RadioButton/__snapshots__/index.jsx.snap | 42 ++- package-lock.json | 348 +++++++++--------- package.json | 1 + .../components/Contentful/Modal/index.jsx | 50 ++- .../Contentful/Modal/modal-styles.scss | 188 ++++++++++ .../components/Contentful/Modal/style.scss | 6 + .../GUIKit/Assets/Images/icon-next.svg | 2 +- .../GUIKit/Assets/Images/icon-prev.svg | 2 +- .../components/GUIKit/Checkbox/index.jsx | 6 +- .../components/GUIKit/Checkbox/style.scss | 15 + .../components/GUIKit/Datepicker/index.jsx | 7 +- .../components/GUIKit/Datepicker/style.scss | 157 +++++++- .../components/GUIKit/Dropdown/style.scss | 1 + .../GUIKit/DropdownTerms/style.scss | 1 + .../components/GUIKit/FilePicker/index.jsx | 77 ++++ .../components/GUIKit/FilePicker/styles.scss | 57 +++ .../components/GUIKit/RadioButton/index.jsx | 53 +-- .../components/GUIKit/RadioButton/style.scss | 24 +- src/shared/components/Gigs/GigApply/index.jsx | 255 +++++++++++++ .../components/Gigs/GigApply/style.scss | 225 +++++++++++ .../{GigDetails.jsx => GigDetails/index.jsx} | 2 +- .../Gigs/{ => GigDetails}/style.scss | 0 .../containers/Gigs/RecruitCRMJobApply.jsx | 158 ++++++++ .../containers/Gigs/RecruitCRMJobDetails.jsx | 11 +- src/shared/containers/Gigs/techSkills.js | 1 + src/shared/containers/GigsPages.jsx | 7 +- .../SubscribeMailChimpTag/style.scss | 1 + src/shared/routes/index.jsx | 5 + 29 files changed, 1455 insertions(+), 251 deletions(-) create mode 100644 src/shared/components/Contentful/Modal/modal-styles.scss create mode 100644 src/shared/components/GUIKit/FilePicker/index.jsx create mode 100644 src/shared/components/GUIKit/FilePicker/styles.scss create mode 100644 src/shared/components/Gigs/GigApply/index.jsx create mode 100644 src/shared/components/Gigs/GigApply/style.scss rename src/shared/components/Gigs/{GigDetails.jsx => GigDetails/index.jsx} (97%) rename src/shared/components/Gigs/{ => GigDetails}/style.scss (100%) create mode 100644 src/shared/containers/Gigs/RecruitCRMJobApply.jsx create mode 100644 src/shared/containers/Gigs/techSkills.js diff --git a/__tests__/shared/components/GUIKit/Datepicker/__snapshots__/index.jsx.snap b/__tests__/shared/components/GUIKit/Datepicker/__snapshots__/index.jsx.snap index 6bf8ad76cf..8ec048cd49 100644 --- a/__tests__/shared/components/GUIKit/Datepicker/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/GUIKit/Datepicker/__snapshots__/index.jsx.snap @@ -2,7 +2,7 @@ exports[`Default render 1`] = `
} date={null} - daySize={53} + daySize={47} disableScroll={false} disabled={false} displayFormat="MMM DD, YYYY" diff --git a/__tests__/shared/components/GUIKit/RadioButton/__snapshots__/index.jsx.snap b/__tests__/shared/components/GUIKit/RadioButton/__snapshots__/index.jsx.snap index 29192cd7c7..f7455f2db2 100644 --- a/__tests__/shared/components/GUIKit/RadioButton/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/GUIKit/RadioButton/__snapshots__/index.jsx.snap @@ -1,29 +1,31 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Default render 1`] = ` -
+
- - - radio - + className="src-shared-components-GUIKit-RadioButton-___style__label___MCxnk" + > + radio + +
-
+ `; diff --git a/package-lock.json b/package-lock.json index 9835410199..5118e651b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2235,12 +2235,9 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==" }, "attr-accept": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz", - "integrity": "sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==", - "requires": { - "core-js": "^2.5.0" - } + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz", + "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==" }, "auth0-js": { "version": "9.13.2", @@ -8312,6 +8309,11 @@ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, + "fast-safe-stringify": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.0.7.tgz", + "integrity": "sha512-Utm6CdzT+6xsDk2m8S6uL8VHxNwI6Jub+e9NYTcAms28T84pTa25GJQV9j0CY0N1rM8hK4x6grpF2BQf+2qwVA==" + }, "fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -8396,6 +8398,21 @@ "schema-utils": "^0.4.5" } }, + "file-selector": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.1.13.tgz", + "integrity": "sha512-T2efCBY6Ps+jLIWdNQsmzt/UnAjKOEAlsZVdnQztg/BtAZGNL4uX1Jet9cMM8gify/x4CSudreji2HssGBNVIQ==", + "requires": { + "tslib": "^2.0.1" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, "file-set": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/file-set/-/file-set-2.0.1.tgz", @@ -18088,12 +18105,13 @@ } }, "react-dropzone": { - "version": "3.13.4", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-3.13.4.tgz", - "integrity": "sha1-hNomgVxAM5aRxJtFRMLvehaRLMw=", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-11.2.0.tgz", + "integrity": "sha512-S/qaXQHCCg7MVlcrhqd05MLC6DupITLUB0CFn3iCLs6OTjzxdGDF1WTktTe5Jyq8jZdxYfMHNUZOHL0mg+K0Dw==", "requires": { - "attr-accept": "^1.0.3", - "prop-types": "^15.5.7" + "attr-accept": "^2.0.0", + "file-selector": "^0.1.12", + "prop-types": "^15.7.2" } }, "react-fast-compare": { @@ -20392,7 +20410,8 @@ "semver-compare": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz", - "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=" + "integrity": "sha1-De4hahyUGrN+nvsXiPavxf9VN/w=", + "dev": true }, "semver-regex": { "version": "2.0.0", @@ -22896,12 +22915,10 @@ "filestack-js": "^1.13.2", "formsy-react": "^0.19.5", "isomorphic-fetch": "^2.2.1", - "libphonenumber-js": "1.4.6", "lodash": "^4.0.0", "material-ui": "^0.20.2", "moment": "^2.11.2", "prop-types": "^15.7.2", - "rc-slider": "8.6.4", "react": "^15.3.1", "react-addons-pure-render-mixin": "^15.3.1", "react-addons-update": "^15.3.1", @@ -22918,7 +22935,6 @@ "react-textarea-autosize": "^5.2.1", "react-transition-group": "^2.2.1", "redux-thunk": "^2.1.0", - "tc-ui": "git+https://github.com/appirio-tech/tc-ui.git#feature/connectv2", "uncontrollable": "^4.0.1" }, "dependencies": { @@ -22965,16 +22981,6 @@ "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" }, - "libphonenumber-js": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.4.6.tgz", - "integrity": "sha512-TD1DyhPjVfNNiIxhwsooCO5j9L6JB60Qd+rlIEItgw8RKEqezu8Tva3V/4wrBiYMnBOHkp3uyzAe/PT9omyUdw==", - "requires": { - "minimist": "^1.2.0", - "semver-compare": "^1.0.0", - "xml2js": "^0.4.17" - } - }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -22983,11 +22989,6 @@ "js-tokens": "^3.0.0 || ^4.0.0" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" - }, "moment": { "version": "2.24.0", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", @@ -23016,20 +23017,6 @@ "strict-uri-encode": "^1.0.0" } }, - "rc-slider": { - "version": "8.6.4", - "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-8.6.4.tgz", - "integrity": "sha512-CV2i2Ww6ib0EjFuBKvgjw3PgT6QwvWKC93iEpqPtrztZrx5wO9Iw//AUri4KHRqptW13AuBvFdEHovqLi6XFTw==", - "requires": { - "babel-runtime": "6.x", - "classnames": "^2.2.5", - "prop-types": "^15.5.4", - "rc-tooltip": "^3.7.0", - "rc-util": "^4.0.4", - "shallowequal": "^1.0.1", - "warning": "^3.0.0" - } - }, "react-router": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/react-router/-/react-router-2.8.1.tgz", @@ -23042,6 +23029,40 @@ "warning": "^3.0.0" } }, + "tc-ui": { + "version": "git+https://github.com/appirio-tech/tc-ui.git#e577a0e704136f1e9ecce92ce4c0626aab932691", + "from": "git+https://github.com/appirio-tech/tc-ui.git#e577a0e704136f1e9ecce92ce4c0626aab932691", + "requires": { + "classnames": "^2.2.3", + "lodash": "^4.0.0", + "moment": "^2.11.2", + "node-neat": "~1.7.1-beta1", + "react": "^0.14.7", + "react-datetime": "^2.0.2", + "react-dom": "^0.14.7", + "react-dropzone": "^3.3.2", + "react-redux": "^4.2.1", + "react-router": "^2.0.0-rc6", + "react-select": "^0.9.1", + "redux": "^3.3.1" + }, + "dependencies": { + "react": { + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/react/-/react-0.14.9.tgz", + "integrity": "sha1-kRCmSXxJ1EuhwO3TF67CnC4NkdE=", + "requires": { + "envify": "^3.0.0", + "fbjs": "^0.6.1" + } + }, + "react-dom": { + "version": "0.14.9", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-0.14.9.tgz", + "integrity": "sha1-BQZKPc8PsYgKOyv8nVjFXY2fYpM=" + } + } + }, "warning": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", @@ -23049,20 +23070,6 @@ "requires": { "loose-envify": "^1.0.0" } - }, - "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", - "requires": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - } - }, - "xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" } } }, @@ -31603,111 +31610,6 @@ "inherits": "2" } }, - "tc-ui": { - "version": "git+https://github.com/appirio-tech/tc-ui.git#e577a0e704136f1e9ecce92ce4c0626aab932691", - "from": "git+https://github.com/appirio-tech/tc-ui.git#feature/connectv2", - "requires": { - "classnames": "^2.2.3", - "lodash": "^4.0.0", - "moment": "^2.11.2", - "node-neat": "~1.7.1-beta1", - "react": "^0.14.7", - "react-datetime": "^2.0.2", - "react-dom": "^0.14.7", - "react-dropzone": "^3.3.2", - "react-redux": "^4.2.1", - "react-router": "^2.0.0-rc6", - "react-select": "^0.9.1", - "redux": "^3.3.1" - }, - "dependencies": { - "fbjs": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.6.1.tgz", - "integrity": "sha1-lja3cF9bqWhNRLcveDISVK/IYPc=", - "requires": { - "core-js": "^1.0.0", - "loose-envify": "^1.0.0", - "promise": "^7.0.3", - "ua-parser-js": "^0.7.9", - "whatwg-fetch": "^0.9.0" - } - }, - "history": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/history/-/history-2.1.2.tgz", - "integrity": "sha1-SqLeiXoOSGfkU5hDvm7Nsphr/ew=", - "requires": { - "deep-equal": "^1.0.0", - "invariant": "^2.0.0", - "query-string": "^3.0.0", - "warning": "^2.0.0" - }, - "dependencies": { - "warning": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-2.1.0.tgz", - "integrity": "sha1-ISINnGOvx3qMkhEeARr3Bc4MaQE=", - "requires": { - "loose-envify": "^1.0.0" - } - } - } - }, - "hoist-non-react-statics": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz", - "integrity": "sha1-qkSM8JhtVcxAdzsXF0t90GbLfPs=" - }, - "moment": { - "version": "2.27.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz", - "integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==" - }, - "query-string": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-3.0.3.tgz", - "integrity": "sha1-ri4UtNBQcdTpuetIc8NbDc1C5jg=", - "requires": { - "strict-uri-encode": "^1.0.0" - } - }, - "react": { - "version": "0.14.9", - "resolved": "https://registry.npmjs.org/react/-/react-0.14.9.tgz", - "integrity": "sha1-kRCmSXxJ1EuhwO3TF67CnC4NkdE=", - "requires": { - "envify": "^3.0.0", - "fbjs": "^0.6.1" - } - }, - "react-dom": { - "version": "0.14.9", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-0.14.9.tgz", - "integrity": "sha1-BQZKPc8PsYgKOyv8nVjFXY2fYpM=" - }, - "react-router": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-2.8.1.tgz", - "integrity": "sha1-c+lJH2zrMW0Pd5gpCBhj43juTtc=", - "requires": { - "history": "^2.1.2", - "hoist-non-react-statics": "^1.2.0", - "invariant": "^2.2.1", - "loose-envify": "^1.2.0", - "warning": "^3.0.0" - } - }, - "warning": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/warning/-/warning-3.0.0.tgz", - "integrity": "sha1-MuU3fLVy3kqwR1O9+IIcAe1gW3w=", - "requires": { - "loose-envify": "^1.0.0" - } - } - } - }, "tcomb": { "version": "3.2.29", "resolved": "https://registry.npmjs.org/tcomb/-/tcomb-3.2.29.tgz", @@ -32553,6 +32455,21 @@ "redux": "^3.3.1" }, "dependencies": { + "attr-accept": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-1.1.3.tgz", + "integrity": "sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==", + "requires": { + "core-js": "^2.5.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + } + } + }, "core-js": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz", @@ -32621,6 +32538,15 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-0.14.9.tgz", "integrity": "sha1-BQZKPc8PsYgKOyv8nVjFXY2fYpM=" }, + "react-dropzone": { + "version": "3.13.4", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-3.13.4.tgz", + "integrity": "sha1-hNomgVxAM5aRxJtFRMLvehaRLMw=", + "requires": { + "attr-accept": "^1.0.3", + "prop-types": "^15.5.7" + } + }, "react-input-autosize": { "version": "0.6.13", "resolved": "https://registry.npmjs.org/react-input-autosize/-/react-input-autosize-0.6.13.tgz", @@ -33204,9 +33130,9 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "topcoder-react-lib": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/topcoder-react-lib/-/topcoder-react-lib-1.0.3.tgz", - "integrity": "sha512-cb4QLW2m3CqzJHWkfbx5TM/rV1bpJXk3FqetNIyKETipebkXtbL83mxtkaV3IEDYrpeKmfzDwjmbxwBh8Gopkw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/topcoder-react-lib/-/topcoder-react-lib-1.0.5.tgz", + "integrity": "sha512-vit+BVaU5SbjwTzLDAqFxE2P539+huV6QspsV3Ud5fL12HQqu3cpJg8iZLmEcCPSXonocVyhtvqdrSgqF1oB4Q==", "requires": { "auth0-js": "^6.8.4", "config": "^3.2.0", @@ -33265,6 +33191,14 @@ "json5": "^2.1.1" } }, + "debug": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz", + "integrity": "sha512-IX2ncY78vDTjZMFUdmsvIRFY2Cf4FnD0wRs+nQwJU8Lu99/tPFdb0VybiiMTPe3I6rQmwsqQqRBvxU+bZ/I8sg==", + "requires": { + "ms": "2.1.2" + } + }, "follow-redirects": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-0.0.7.tgz", @@ -33272,6 +33206,31 @@ "requires": { "debug": "^2.2.0", "stream-consume": "^0.1.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + } + } + }, + "form-data": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" } }, "hoist-non-react-statics": { @@ -33290,6 +33249,16 @@ "minimist": "^1.2.5" } }, + "mime": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==" + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, "object-keys": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-0.4.0.tgz", @@ -33308,11 +33277,44 @@ "react-is": "^16.8.2" } }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + }, "serialize-javascript": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==" }, + "superagent": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-5.3.1.tgz", + "integrity": "sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==", + "requires": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.2", + "debug": "^4.1.1", + "fast-safe-stringify": "^2.0.7", + "form-data": "^3.0.0", + "formidable": "^1.2.2", + "methods": "^1.1.2", + "mime": "^2.4.6", + "qs": "^6.9.4", + "readable-stream": "^3.6.0", + "semver": "^7.3.2" + } + }, "tc-core-library-js": { "version": "github:appirio-tech/tc-core-library-js#d16413db30b1eed21c0cf426e185bedb2329ddab", "from": "github:appirio-tech/tc-core-library-js#v2.6", @@ -33329,15 +33331,15 @@ }, "dependencies": { "auth0-js": { - "version": "9.13.4", - "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.13.4.tgz", - "integrity": "sha512-G7wXTtEUe8OG5UMdcFPoS47odorEZ3WerNyWLLhoGlLqYcPgv0t+B0ECHv/rVLULbpctbSBrRFFYa43/bJV4+Q==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/auth0-js/-/auth0-js-9.14.0.tgz", + "integrity": "sha512-40gIBUejmYAYse06ck6sxdNO0KU0pX+KDIQsWAkcyFtI0HU6dY5aeHxZfVYkYjtbArKr5s13LuZFdKrUiGyCqQ==", "requires": { "base64-js": "^1.3.0", "idtoken-verifier": "^2.0.3", "js-cookie": "^2.2.0", "qs": "^6.7.0", - "superagent": "^3.8.3", + "superagent": "^5.3.1", "url-join": "^4.0.1", "winchan": "^0.2.2" } @@ -33436,9 +33438,9 @@ } }, "topcoder-react-ui-kit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/topcoder-react-ui-kit/-/topcoder-react-ui-kit-2.0.0.tgz", - "integrity": "sha512-9Ph8fRzRjVVB0VH13s8/N4+/ZWPLflrnW7D0fmS+oeDwnQe6k5wMknyFcBCI/pqWHr/11nhclXX7np3LdyWSwA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/topcoder-react-ui-kit/-/topcoder-react-ui-kit-2.0.1.tgz", + "integrity": "sha512-pl8tysSZYHDSbWn8srLZxV+8lK9f32ya8N+yt5C2XF2PKak30Qnqb3PCDhBO1fKmFF90p4ohJRsCi0IxLRyH/A==", "requires": { "prop-types": "^15.6.2", "react": "^16.4.1", diff --git a/package.json b/package.json index 17d01d001b..1fb8f23e5e 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "react-dates": "^18.2.2", "react-dom": "^16.4.1", "react-dotdotdot": "^1.3.1", + "react-dropzone": "^11.2.0", "react-ga": "^2.7.0", "react-helmet": "^5.2.0", "react-html-parser": "^2.0.2", diff --git a/src/shared/components/Contentful/Modal/index.jsx b/src/shared/components/Contentful/Modal/index.jsx index 196dc72074..55fcd996c2 100644 --- a/src/shared/components/Contentful/Modal/index.jsx +++ b/src/shared/components/Contentful/Modal/index.jsx @@ -9,12 +9,14 @@ import LoadingIndicator from 'components/LoadingIndicator'; import Banner from 'components/Contentful/Banner'; import ContentBlock from 'components/Contentful/ContentBlock'; import Viewport from 'components/Contentful/Viewport'; -import { Modal } from 'topcoder-react-ui-kit'; +import { Modal, PrimaryButton } from 'topcoder-react-ui-kit'; import { errors } from 'topcoder-react-lib'; import { themr } from 'react-css-super-themr'; import classnames from 'classnames'; +import tc from 'components/buttons/themed/tc.scss'; +import { fixStyle } from 'utils/contentful'; -import defaultModalTheme from 'components/Leaderboard/ChallengeHistoryModal/styles.scss'; +import defaultModalTheme from './modal-styles.scss'; import defaultStyle from './style.scss'; const { fireErrorMessage } = errors; @@ -89,6 +91,9 @@ class ContentfulModal extends React.Component { renderPlaceholder={LoadingIndicator} render={(data) => { const contentId = _.get(data, `entries.items.${id}.fields.content.sys.id`); + const hideDismissIcon = _.get(data, `entries.items.${id}.fields.hideDismissIcon`); + const hideCloseButton = _.get(data, `entries.items.${id}.fields.hideCloseButton`); + const extraStylesForContainer = _.get(data, `entries.items.${id}.fields.extraStylesForContainer`); if (!contentId) return null; return ( @@ -107,16 +112,20 @@ class ContentfulModal extends React.Component { onCancel={this.onCloseModal} theme={defaultModalTheme} > -
{ this.dismissButtonRef = node; }} - > - × -
+ { + !hideDismissIcon && ( +
{ this.dismissButtonRef = node; }} + > + × +
+ ) + } + { + !hideCloseButton && ( +
+ + CLOSE + +
+ ) + } )}
@@ -160,6 +185,7 @@ ContentfulModal.propTypes = { theme: PT.shape({ modalTrigger: PT.string, dismissButton: PT.any, + closeButton: PT.any, }), preview: PT.bool, spaceName: PT.string, diff --git a/src/shared/components/Contentful/Modal/modal-styles.scss b/src/shared/components/Contentful/Modal/modal-styles.scss new file mode 100644 index 0000000000..8e3bf60873 --- /dev/null +++ b/src/shared/components/Contentful/Modal/modal-styles.scss @@ -0,0 +1,188 @@ +@import "~styles/mixins"; + +$light-gray: #d4d4d4; + +.container { + @include roboto-regular; + + color: $tc-gray-50; + background: $tc-white; + border-radius: 10px; + top: 50%; + width: auto; + max-height: 90%; + overflow-y: auto; + + @media (max-width: 768px) { + width: 80%; + } + + h3 { + color: #1e94a3; + font-family: BarlowCondensed, sans-serif; + font-size: 34px; + font-weight: 500; + line-height: 38px; + text-align: center; + margin-bottom: 60px; + text-transform: uppercase; + + @media (max-width: 768px) { + font-size: 31px !important; + font-weight: 500 !important; + line-height: 33px !important; + margin-bottom: 30px; + } + } +} + +.overlay { + background-color: #2a2a2a; + opacity: 0.95; + border: none; + height: 100%; + left: 0; + outline: none; + position: fixed; + top: 0; + width: 100%; + z-index: 998; +} + +.podium-spot-wrapper { + text-align: center; + display: flex; + justify-content: center; + margin-bottom: 50px; + + > div > span { + height: 128px; + width: 128px; + + img, + svg { + border-radius: 50%; + border-style: solid; + border-width: 3px; + height: 100%; + width: 100%; + } + } +} + +.history-table { + width: 100%; + margin-bottom: 62px; + + thead { + th { + color: #2a2a2a; + font-family: Roboto, sans-serif; + font-size: 14px; + font-weight: 500; + letter-spacing: 0.5px; + line-height: 18px; + text-align: left; + text-transform: uppercase; + border-bottom: 1px solid #d4d4d4; + padding-bottom: 15px; + } + } + + .row { + border-bottom: 1px solid #d4d4d4; + + .name { + .link { + color: #0d61bf; + font-size: 14px; + font-weight: 400; + line-height: 51px; + text-decoration: underline; + + &:hover { + text-decoration: none; + } + } + } + + .placement { + color: #2a2a2a; + font-size: 14px; + font-weight: 500; + line-height: 51px; + } + + .points { + color: #2a2a2a; + font-size: 14px; + font-weight: 400; + line-height: 51px; + } + } +} + +.buttons { + margin-top: 10px; + display: flex; + flex-direction: row; + justify-content: center; + + .close-btn { + color: #fafafb; + font-family: Roboto, sans-serif; + font-size: 14px; + font-weight: 700; + letter-spacing: 0.8px; + line-height: 40px; + background-color: #137d60; + border-radius: 20px; + border: none; + text-transform: uppercase; + padding: 0 20px; + + &:hover { + background-color: #0ab88a !important; + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2); + } + } +} + +.header-table-content { + display: flex; + align-items: center; +} + +.sort-container { + display: flex; + flex-direction: column; + margin-left: 5px; + padding: 0; + border: none; + outline: none; + background: transparent; +} + +.sort-container > div { + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; +} + +.sort-up { + border-bottom: 4px solid $light-gray; + margin-bottom: 2px; + + &.active { + border-bottom: 4px solid $tc-black; + } +} + +.sort-down { + border-top: 4px solid $light-gray; + + &.active { + border-top: 4px solid $tc-black; + } +} diff --git a/src/shared/components/Contentful/Modal/style.scss b/src/shared/components/Contentful/Modal/style.scss index c94207a613..4e5dea9b8b 100644 --- a/src/shared/components/Contentful/Modal/style.scss +++ b/src/shared/components/Contentful/Modal/style.scss @@ -25,3 +25,9 @@ opacity: 1; } } + +.closeButton { + display: flex; + justify-content: center; + margin: 40px 0 80px 0; +} diff --git a/src/shared/components/GUIKit/Assets/Images/icon-next.svg b/src/shared/components/GUIKit/Assets/Images/icon-next.svg index cb096e3a01..467efa406f 100644 --- a/src/shared/components/GUIKit/Assets/Images/icon-next.svg +++ b/src/shared/components/GUIKit/Assets/Images/icon-next.svg @@ -5,7 +5,7 @@ - + \ No newline at end of file diff --git a/src/shared/components/GUIKit/Assets/Images/icon-prev.svg b/src/shared/components/GUIKit/Assets/Images/icon-prev.svg index 6b3b226ad2..3e90c47088 100644 --- a/src/shared/components/GUIKit/Assets/Images/icon-prev.svg +++ b/src/shared/components/GUIKit/Assets/Images/icon-prev.svg @@ -7,7 +7,7 @@ - + \ No newline at end of file diff --git a/src/shared/components/GUIKit/Checkbox/index.jsx b/src/shared/components/GUIKit/Checkbox/index.jsx index be44e9fcde..4515610909 100644 --- a/src/shared/components/GUIKit/Checkbox/index.jsx +++ b/src/shared/components/GUIKit/Checkbox/index.jsx @@ -15,6 +15,7 @@ function Checkbox({ checked, onChange, size, + errorMsg, }) { const [checkedInternal, setCheckedInternal] = useState(checked); let sizeStyle = size === 'lg' ? 'lgSize' : null; @@ -37,9 +38,10 @@ function Checkbox({ delayedOnChange(e.target.checked, onChange); }} /> -
+
+ {errorMsg ? ({errorMsg}) : null} ); } @@ -48,12 +50,14 @@ Checkbox.defaultProps = { checked: false, onChange: () => {}, size: 'sm', + errorMsg: '', }; Checkbox.propTypes = { checked: PT.bool, onChange: PT.func, size: PT.oneOf(['xs', 'sm', 'lg']), + errorMsg: PT.string, }; export default Checkbox; diff --git a/src/shared/components/GUIKit/Checkbox/style.scss b/src/shared/components/GUIKit/Checkbox/style.scss index 7d6801f888..1be1148f92 100644 --- a/src/shared/components/GUIKit/Checkbox/style.scss +++ b/src/shared/components/GUIKit/Checkbox/style.scss @@ -8,6 +8,10 @@ background-color: $tc-white; border: 1px solid $gui-kit-gray-30; + &.haveError { + border: 2px solid #ef476f; + } + /* Create the checkmark/indicator (hidden when not checked) */ .after { position: absolute; @@ -107,3 +111,14 @@ } } } + +.errorMessage { + display: block; + + @include errorMessage; + + position: absolute; + white-space: nowrap; + margin: 35px 0 0 0; + color: #ef476f; +} diff --git a/src/shared/components/GUIKit/Datepicker/index.jsx b/src/shared/components/GUIKit/Datepicker/index.jsx index 53bc0887ae..5cf376d7ef 100644 --- a/src/shared/components/GUIKit/Datepicker/index.jsx +++ b/src/shared/components/GUIKit/Datepicker/index.jsx @@ -11,9 +11,13 @@ import 'react-dates/initialize'; import { SingleDatePicker } from 'react-dates'; import IconCalendar from 'assets/images/tc-edu/icon-calendar.svg'; import useWindowSize from 'utils/useWindowSize'; +import CalendarWeek from 'react-dates/lib/components/CalendarWeek'; import IconNext from '../Assets/Images/icon-next.svg'; import IconPrev from '../Assets/Images/icon-prev.svg'; +// eslint-disable-next-line no-unused-expressions, react/forbid-foreign-prop-types +CalendarWeek && CalendarWeek.propTypes && delete CalendarWeek.propTypes.children; // fixing the bug in react-dates, more detail in here https://github.com/airbnb/react-dates/issues/1121 + function Datepicker({ value, placeholder, @@ -27,7 +31,6 @@ function Datepicker({ const { width } = useWindowSize(); return (
} navNext={} displayFormat="MMM DD, YYYY" - daySize={width > 600 ? 53 : 35} + daySize={width > 600 ? 47 : 35} renderDayContents={d => (
{d.date ? d.date() : ''}
)} enableOutsideDays firstDayOfWeek={1} diff --git a/src/shared/components/GUIKit/Datepicker/style.scss b/src/shared/components/GUIKit/Datepicker/style.scss index 25a0f7a9e5..320798b212 100644 --- a/src/shared/components/GUIKit/Datepicker/style.scss +++ b/src/shared/components/GUIKit/Datepicker/style.scss @@ -4,6 +4,7 @@ position: relative; display: flex; flex-direction: column; + flex-grow: 1; .label { @include textInputLabel; @@ -26,6 +27,12 @@ :global { @import '~react-dates/lib/css/_datepicker.css'; + .SingleDatePicker { + width: 100%; + position: relative; + display: inline-block; + } + button { &:focus { outline: none; @@ -34,6 +41,8 @@ .SingleDatePickerInput { position: relative; + border: none; + width: 100%; .SingleDatePickerInput_calendarIcon { position: absolute; @@ -44,9 +53,29 @@ width: 48px; bottom: 0; z-index: 1; + background: 0 0; + border: 0; + color: inherit; + font: inherit; + line-height: normal; + overflow: visible; + cursor: pointer; + display: inline-block; + vertical-align: middle; } .DateInput { + padding: 0; + width: 100%; + font-family: "Roboto", Helvetica, Arial, sans-serif; + font-weight: 400; + font-size: 15px; + margin: 0; + background: #fff; + position: relative; + display: inline-block; + vertical-align: middle; + input { @include textInput; @@ -57,32 +86,103 @@ .DateInput_fang { display: none; } + + .DateInput_screenReaderMessage { + border: 0; + clip: rect(0, 0, 0, 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + } } .SingleDatePicker_picker { z-index: 7; top: 64px !important; + background-color: #fff; + position: absolute; + + .DayPicker { + background: #fff; + position: relative; + text-align: left; + + .DayPicker_weekHeaders__horizontal { + margin-left: 9px; + + .DayPicker_weekHeader { + color: #757575; + position: absolute; + top: 62px; + z-index: 2; + text-align: left; + + .DayPicker_weekHeader_ul { + list-style: none; + margin: 1px 0; + padding-left: 0; + padding-right: 0; + font-size: 14px; + + .DayPicker_weekHeader_ul { + list-style: none; + margin: 1px 0; + padding-left: 0; + padding-right: 0; + font-size: 14px; + display: inline-block; + text-align: center; + } + } + } + } + + .DayPicker_weekHeaders { + position: relative; + + .DayPicker_weekHeader_li { + font-size: 15px; + font-family: "Roboto", Helvetica, Arial, sans-serif; + font-weight: 400; + line-height: 20px; + display: inline-block; + text-align: center; + } + } + + .DayPicker_focusRegion { + outline: 0; + } + } .DayPicker__withBorder { border: 1px solid $gui-kit-gray-30; box-shadow: 2px 2px 3px 0 $tc-gray-neutral-light; overflow: hidden; + border-radius: 3px; + } + + .DayPickerNavigation__horizontal { + height: 0; + } + + .DayPickerNavigation { + position: relative; + z-index: 2; } .DayPickerNavigation_button { position: absolute; top: 18px; + cursor: pointer; &:focus { outline: none; } - svg { - path { - fill: $gui-kit-level-2; - } - } - &:first-child { left: 18px; } @@ -99,6 +199,9 @@ padding-bottom: 10px; margin-bottom: 45px; position: relative; + color: $gui-kit-gray-90; + text-align: center; + caption-side: initial; &::after { content: ''; @@ -107,16 +210,17 @@ left: -5px; width: calc(100% + 10px); height: 1px; - background-color: $tc-gray-20; + background-color: #d4d4d4; } } - + /* stylelint-disable */ .DayPicker_weekHeader { .DayPicker_weekHeader_ul { .DayPicker_weekHeader_li { small { color: $gui-kit-gray-90; font-weight: 500 !important; + font-size: 16px; } } } @@ -124,14 +228,46 @@ .DayPicker_focusRegion { .DayPicker_transitionContainer { + position: relative; + overflow: hidden; + border-radius: 3px; + &.DayPicker_transitionContainer__horizontal { transition: none !important; } .CalendarMonthGrid { + background: #fff; + text-align: left; + z-index: 0; + position: absolute; + left: 9px; + .CalendarMonthGrid_month__horizontal { + display: inline-block; + vertical-align: top; + min-height: 100%; + + &.CalendarMonthGrid_month__hidden { + visibility: hidden; + } + + &.CalendarMonthGrid_month__hideForAnimation { + position: absolute; + z-index: -1; + opacity: 0; + pointer-events: none; + } + .CalendarMonth { + background: #fff; + text-align: center; + vertical-align: top; + user-select: none; + table.CalendarMonth_table { + margin-bottom: 0; + tbody { tr { td { @@ -155,6 +291,7 @@ &.CalendarDay__default { color: #2a2a2a; + padding: 0; } &.CalendarDay__blocked_out_of_range { @@ -171,11 +308,12 @@ } &.CalendarDay__outside { - color: $tc-gray-20; + color: $gui-kit-gray-30; } &:not(.CalendarDay__outside):not(.CalendarDay__blocked_out_of_range):not(.CalendarDay__selected):hover { background: transparent !important; + cursor: pointer; div { background: $tc-gray-05; @@ -190,6 +328,7 @@ } } } + /* stylelint-enable */ } } } diff --git a/src/shared/components/GUIKit/Dropdown/style.scss b/src/shared/components/GUIKit/Dropdown/style.scss index 7ca3bf762b..8f58c28383 100644 --- a/src/shared/components/GUIKit/Dropdown/style.scss +++ b/src/shared/components/GUIKit/Dropdown/style.scss @@ -147,6 +147,7 @@ line-height: 30px !important; color: $gui-kit-gray-90 !important; background-color: transparent !important; + text-decoration: none !important; &.is-selected { font-weight: bold !important; diff --git a/src/shared/components/GUIKit/DropdownTerms/style.scss b/src/shared/components/GUIKit/DropdownTerms/style.scss index 121aa54da7..d06e106b10 100644 --- a/src/shared/components/GUIKit/DropdownTerms/style.scss +++ b/src/shared/components/GUIKit/DropdownTerms/style.scss @@ -149,6 +149,7 @@ :global { .Select-menu-outer { border: none !important; + padding-bottom: 10px !important; } } } diff --git a/src/shared/components/GUIKit/FilePicker/index.jsx b/src/shared/components/GUIKit/FilePicker/index.jsx new file mode 100644 index 0000000000..19d9fa1050 --- /dev/null +++ b/src/shared/components/GUIKit/FilePicker/index.jsx @@ -0,0 +1,77 @@ +/** + * GUIKit FilePicker based on filestack-react + */ + +import React from 'react'; +import PT from 'prop-types'; +import Dropzone from 'react-dropzone'; + +import './styles.scss'; + +/** + * FilestackFilePicker component + */ +function FilestackFilePicker({ + onFilePick, + btnText, + infoText, + options, + errorMsg, + inputOptions, + file, +}) { + let fileName = file ? file.name : null; + return ( + + { + fileName = acceptedFiles[0].name; + onFilePick(acceptedFiles); + }} + {...options} + > + {({ getRootProps, getInputProps }) => ( +
+ + { + fileName ? ( +

{fileName}

+ ) : ( +

+ {infoText} + OR +

+ ) + } + +
+ )} +
+ {errorMsg ? ({errorMsg}) : null} +
+ ); +} + +FilestackFilePicker.defaultProps = { + infoText: '', + btnText: 'SELECT A FILE', + options: {}, + errorMsg: '', + inputOptions: {}, + file: null, +}; + +/** + * Prop Validation + */ +FilestackFilePicker.propTypes = { + infoText: PT.string, + btnText: PT.string, + onFilePick: PT.func.isRequired, + options: PT.shape(), + errorMsg: PT.string, + inputOptions: PT.shape(), + file: PT.shape(), +}; + +export default FilestackFilePicker; diff --git a/src/shared/components/GUIKit/FilePicker/styles.scss b/src/shared/components/GUIKit/FilePicker/styles.scss new file mode 100644 index 0000000000..9906a1e66e --- /dev/null +++ b/src/shared/components/GUIKit/FilePicker/styles.scss @@ -0,0 +1,57 @@ +@import '~components/buttons/themed/tc.scss'; +@import '~components/GUIKit/Assets/Styles/default'; + +.container { + display: flex; + align-items: center; + justify-content: center; + flex-direction: column; + background-color: #fff; + border-radius: 6px; + min-height: 164px; + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='gray' stroke-width='1' stroke-dasharray='5%2c5' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e"); + outline: none !important; + padding: 25px 15px 40px 15px; + + &.hasError { + background-image: url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='6' ry='6' stroke='red' stroke-width='1' stroke-dasharray='5%2c5' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e"); + } + + .btn { + outline: none !important; + + @include primary-white; + @include sm; + + &:hover { + @include primary-white; + } + } + + .infoText { + color: #aaa; + font-family: Roboto, sans-serif; + font-size: 16px !important; + line-height: 22px !important; + margin: 0 !important; + display: flex; + flex-direction: column; + align-items: center; + + > span { + display: flex; + margin: 19px 0; + } + + &.withFile { + color: #2a2a2a; + margin: 40px 0 !important; + } + } +} + +.errorMessage { + display: block; + + @include errorMessage; +} diff --git a/src/shared/components/GUIKit/RadioButton/index.jsx b/src/shared/components/GUIKit/RadioButton/index.jsx index fb2df581fb..cf0744294b 100644 --- a/src/shared/components/GUIKit/RadioButton/index.jsx +++ b/src/shared/components/GUIKit/RadioButton/index.jsx @@ -10,7 +10,9 @@ import './style.scss'; import { config } from 'topcoder-react-utils'; -function RadioButton({ options, onChange, size }) { +function RadioButton({ + options, onChange, size, errorMsg, +}) { const [internalOptions, setInternalOptions] = useState(options); const optionsWithKey = internalOptions.map((o, oIndex) => ({ ...o, key: oIndex })); let sizeStyle = size === 'lg' ? 'lgSize' : null; @@ -22,34 +24,38 @@ function RadioButton({ options, onChange, size }) { ).current; return ( -
- {optionsWithKey.map(o => ( -
- - {o.label ? ({o.label}) : null} -
- ))} -
+ +
+ {optionsWithKey.map(o => ( +
+ + {o.label ? ({o.label}) : null} +
+ ))} +
+ {errorMsg ? ({errorMsg}) : null} +
); } RadioButton.defaultProps = { onChange: () => {}, size: 'sm', + errorMsg: '', }; RadioButton.propTypes = { @@ -61,6 +67,7 @@ RadioButton.propTypes = { ).isRequired, onChange: PT.func, size: PT.oneOf(['xs', 'sm', 'lg']), + errorMsg: PT.string, }; export default RadioButton; diff --git a/src/shared/components/GUIKit/RadioButton/style.scss b/src/shared/components/GUIKit/RadioButton/style.scss index 63705c163d..cbe0ebdaa9 100644 --- a/src/shared/components/GUIKit/RadioButton/style.scss +++ b/src/shared/components/GUIKit/RadioButton/style.scss @@ -24,6 +24,10 @@ background-color: $tc-white; box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.35); } + + &.hasError { + border: 2px solid $gui-kit-level-5; + } } .radioButton { @@ -33,6 +37,7 @@ .label { font-size: 14px; + cursor: pointer; } /* The container */ @@ -70,6 +75,10 @@ display: flex; flex-direction: column; + .label { + color: $gui-kit-gray-90; + } + // lg size &.lgSize { .container { @@ -91,7 +100,7 @@ } .label { - margin-left: 15px; + margin-left: 8px; } } @@ -116,7 +125,7 @@ } .label { - margin-left: 10px; + margin-left: 8px; } } @@ -141,7 +150,16 @@ } .label { - margin-left: 10px; + margin-left: 8px; } } } + +.errorMessage { + display: block; + + @include errorMessage; + + color: #ef476f; + margin-left: 0; +} diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx new file mode 100644 index 0000000000..889c45eb3b --- /dev/null +++ b/src/shared/components/Gigs/GigApply/index.jsx @@ -0,0 +1,255 @@ +/** + * The Gig apply page. + */ + +import _ from 'lodash'; +import React from 'react'; +import PT from 'prop-types'; +import { Link, config } from 'topcoder-react-utils'; +import TextInput from 'components/GUIKit/TextInput'; +import Datepicker from 'components/GUIKit/Datepicker'; +import DropdownTerms from 'components/GUIKit/DropdownTerms'; +import RadioButton from 'components/GUIKit/RadioButton'; +import Checkbox from 'components/GUIKit/Checkbox'; +import { getCustomField } from 'utils/gigs'; +import Modal from 'components/Contentful/Modal'; +import FilestackFilePicker from 'components/GUIKit/FilePicker'; +import './style.scss'; + +export default function GigApply(props) { + const { + job, onFormInputChange, formData, formErrors, onApplyClick, + } = props; + + return ( +
+ { + job.error || job.enable_job_application_form !== 1 ? ( +
+

Gig does not exist.

+
+ VIEW OTHER GIGS +
+
+ ) : ( +
+

{job.name}

+ GIG DETAILS +
+
+

PERSONAL INFORMATION

+

Welcome to Topcoder Gigs! We’d like to get to know you.

+
+
+ onFormInputChange('fname', val)} + errorMsg={formErrors.fname} + value={formData.fname} + required + /> + onFormInputChange('lname', val)} + errorMsg={formErrors.lname} + value={formData.lname} + required + /> +
+
+ onFormInputChange('email', val)} + errorMsg={formErrors.email} + value={formData.email} + required + /> + onFormInputChange('phone', val)} + errorMsg={formErrors.phone} + value={formData.phone} + required + /> +
+
+ onFormInputChange('city', val)} + errorMsg={formErrors.city} + value={formData.city} + required + /> + onFormInputChange('country', val)} + errorMsg={formErrors.country} + value={formData.country} + required + /> +
+
+

TOPCODER INFORMATION

+

If you have a Topcoder profile, please share. Not a Member?

+
+
+ onFormInputChange('handle', val)} + errorMsg={formErrors.handle} + value={formData.handle} + /> + onFormInputChange('tcProfileLink', val)} + errorMsg={formErrors.tcProfileLink} + value={formData.handle ? `topcoder.com/members/${formData.handle}` : null} + /> +
+
+

SHARE YOUR EXPECTATIONS

+

Your Professional Work History

+
+
+ onFormInputChange('payExpectation', val)} + errorMsg={formErrors.Expectation} + /> + onFormInputChange('availFrom', val)} + errorMsg={formErrors.availFrom} + value={formData.availFrom} + /> +
+
+

RESUME & SKILLS

+

Upload Your Resume or CV

+
+ onFormInputChange('fileCV', files[0])} + inputOptions={{ + accept: '.pdf,.docx', + }} + infoText="Drag & drop your resume or CV here - please omit contact information *" + errorMsg={formErrors.fileCV} + /> +
+ onFormInputChange('skills', val)} + errorMsg={formErrors.skills} + addNewOptionPlaceholder="Type to add another skill..." + required + /> +
+

FINAL QUESTIONS

+

Please Complete the Following Questions

+
+ onFormInputChange('reffereal', val)} + errorMsg={formErrors.reffereal} + value={formData.reffereal} + required + /> +
+ onFormInputChange('whyFit', val)} + errorMsg={formErrors.whyFit} + /> +

What is your availability per week?

+
+ { + _.map(formData.timeAvailability, (cbox, indx) => ( +
+ { + formData.timeAvailability[indx].checked = val; + onFormInputChange('timeAvailability', formData.timeAvailability); + }} + checked={formData.timeAvailability[indx].checked} + size="lg" + /> + {cbox.label} +
+ )) + } +
+

Are you able to work during the specified timezone? ({`${getCustomField(job.custom_fields, 'Timezone')}`})

+ onFormInputChange('timezoneConfirm', val)} + errorMsg={formErrors.timezoneConfirm} + options={formData.timezoneConfirm} + size="lg" + /> +

Are you ok to work with the duration of the gig? ({`${getCustomField(job.custom_fields, 'Duration')}`})

+ onFormInputChange('durationConfirm', val)} + errorMsg={formErrors.durationConfirm} + options={formData.durationConfirm} + size="lg" + /> +
+ onFormInputChange('notes', val)} + errorMsg={formErrors.notes} + /> +
+
+
+
+
+
+ onFormInputChange('agreedTerms', val)} + checked={formData.agreedTerms} + errorMsg={formErrors.agreedTerms} + size="lg" + /> + I agree to Candidate Terms * +
+
+ View Our Equal Employment Opportunity Policy +
+ +
+
+ ) + } +
+ ); +} + +GigApply.defaultProps = { + formErrors: {}, +}; + +GigApply.propTypes = { + job: PT.shape().isRequired, + formErrors: PT.shape(), + formData: PT.shape().isRequired, + onFormInputChange: PT.func.isRequired, + onApplyClick: PT.func.isRequired, +}; diff --git a/src/shared/components/Gigs/GigApply/style.scss b/src/shared/components/Gigs/GigApply/style.scss new file mode 100644 index 0000000000..fd4829d897 --- /dev/null +++ b/src/shared/components/Gigs/GigApply/style.scss @@ -0,0 +1,225 @@ +@import '~styles/mixins'; +@import "~components/Contentful/default"; + +.container { + max-width: $screen-lg; + min-height: 50vh; + margin: auto; + color: #2a2a2a; + + @include gui-kit-headers; + @include gui-kit-content; + @include roboto-regular; + + @include xs-to-md { + padding: 0 15px; + } + + .error { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 27px; + height: 80vh; + } + + .checkboxes-row { + display: flex; + + .checkbox { + display: flex; + align-items: center; + margin-right: 28px; + + &:last-child { + margin-right: 0; + } + + .label { + font-size: 14px; + margin-left: 8px; + } + } + } + + .wrap { + h2 { + color: #26b3c5; + text-align: center; + margin-top: 47px; + margin-bottom: 40px; + + @include xs-to-md { + text-align: left; + } + } + + .back-link { + color: #229174; + font-weight: bold; + font-size: 14px; + letter-spacing: 0.8px; + text-decoration: none; + line-height: 40px; + margin-bottom: 12px; + } + + .separator { + border-bottom: 1px solid #e9e9e9; + } + + .form-wrap { + max-width: 880px; + margin: auto; + + h4 { + margin-top: 35px; + margin-bottom: 5px; + } + + p { + font-size: 14px; + line-height: 22px; + } + + .form-section { + margin: 13px 0 50px; + + .form-row { + display: grid; + gap: 20px; + grid-template-columns: 1fr 1fr; + margin-bottom: 20px; + + @include xs-to-md { + display: flex; + flex-direction: column; + } + } + + p { + margin-top: 30px; + margin-bottom: 6px; + line-height: 30px; + font-size: 16px; + } + + :global { + .radioButtonContainer { + display: flex; + flex-direction: row; + + .radioButton { + margin-right: 28px; + + &:last-child { + margin-right: 0; + } + } + } + } + + .last-input { + margin-top: 34px; + margin-bottom: 81px; + } + + .input-bot-margin { + margin-bottom: 10px; + } + } + } + + .bottom-section { + display: flex; + justify-content: space-between; + font-size: 14px; + margin-top: 23px; + margin-bottom: 80px; + + @include xs-to-md { + flex-direction: column; + + .checkboxes-row, + .checkbox { + margin-bottom: 20px; + } + } + } + + .primaryBtn { + background-color: #137d60; + border-radius: 20px; + color: #fff; + font-size: 14px; + font-weight: bolder; + text-decoration: none; + text-transform: uppercase; + line-height: 40px; + padding: 0 20px; + border: none; + outline: none; + margin: 0 auto 150px auto !important; + display: flex; + + &:hover { + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2); + background-color: #0ab88a; + } + + @include xs-to-sm { + margin-bottom: 20px; + } + + &:disabled { + background-color: #e9e9e9 !important; + border: none !important; + text-decoration: none !important; + color: #fafafb !important; + box-shadow: none !important; + } + } + + .moldal-link { + text-decoration: underline; + color: #0d61bf; + + &:hover { + text-decoration: none; + } + } + } + + .cta-buttons { + display: flex; + justify-content: center; + margin-top: 47px; + + @include xs-to-sm { + flex-direction: column; + } + + /* stylelint-disable */ + a { + background-color: #fff; + border: 1px solid #137d60; + border-radius: 20px; + color: #229174; + font-size: 14px; + font-weight: bolder; + text-decoration: none; + text-transform: uppercase; + line-height: 40px; + padding: 0 20px; + + &:hover { + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2); + } + + @include xs-to-sm { + text-align: center; + } + } + /* stylelint-enable */ + } +} diff --git a/src/shared/components/Gigs/GigDetails.jsx b/src/shared/components/Gigs/GigDetails/index.jsx similarity index 97% rename from src/shared/components/Gigs/GigDetails.jsx rename to src/shared/components/Gigs/GigDetails/index.jsx index df0b06f343..a0be8d26d4 100644 --- a/src/shared/components/Gigs/GigDetails.jsx +++ b/src/shared/components/Gigs/GigDetails/index.jsx @@ -110,7 +110,7 @@ export default function GigDetails(props) {
- APPLY TO THIS JOB + APPLY TO THIS JOB VIEW OTHER JOBS
diff --git a/src/shared/components/Gigs/style.scss b/src/shared/components/Gigs/GigDetails/style.scss similarity index 100% rename from src/shared/components/Gigs/style.scss rename to src/shared/components/Gigs/GigDetails/style.scss diff --git a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx new file mode 100644 index 0000000000..9ea798e0d2 --- /dev/null +++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx @@ -0,0 +1,158 @@ +/** + * Apply for a job page + */ + +import _ from 'lodash'; +// import actions from 'actions/recruitCRM'; +import GigApply from 'components/Gigs/GigApply'; +import PT from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import { isValidEmail } from 'utils/tc'; +import techSkills from './techSkills'; + +class RecruitCRMJobApplyContainer extends React.Component { + constructor(props) { + super(props); + // initial state + this.state = { + formErrors: {}, + formData: _.merge({ + availFrom: new Date(), + skills: _.map(techSkills, label => ({ label, selected: false })), + durationConfirm: [{ label: 'Yes', value: false }, { label: 'No', value: false }], + timezoneConfirm: [{ label: 'Yes', value: false }, { label: 'No', value: false }], + timeAvailability: [ + { label: '10 hours', checked: false }, { label: '20 hours', checked: false }, { label: '30 hours', checked: false }, { label: '40 hours', checked: false }, + ], + agreedTerms: false, + // eslint-disable-next-line react/destructuring-assignment + }, this.props.user), + }; + + // binds + this.onFormInputChange = this.onFormInputChange.bind(this); + this.onApplyClick = this.onApplyClick.bind(this); + this.validateForm = this.validateForm.bind(this); + } + + componentDidMount() { + this.validateForm(); + } + + onFormInputChange(key, value) { + const { formData } = this.state; + // update the state + this.setState({ + formData: { + ...formData, + [key]: value, + }, + }); + // run form validation + this.validateForm(); + } + + // eslint-disable-next-line class-methods-use-this + onApplyClick() { + // eslint-disable-next-line no-console + console.log('onApplyClick'); + } + + validateForm() { + const { formData, formErrors } = this.state; + // Form validation happens here + const requiredTextFields = [ + 'fname', 'lname', 'city', 'country', 'reffereal', 'phone', 'email', + ]; + // check required text fields for value + _.each(requiredTextFields, (key) => { + if (!formData[key] || !_.trim(formData[key])) formErrors[key] = 'Required field'; + else delete formErrors[key]; + }); + // check for valid email + if (formData.email && _.trim(formData.email)) { + if (!(isValidEmail(formData.email))) formErrors.email = 'Invalid email'; + else delete formErrors.email; + } + // require atleast 1 skill + if (!_.find(formData.skills, { selected: true })) formErrors.skills = 'Please, add technical skills'; + else delete formErrors.skills; + // have accepted terms + if (!formData.agreedTerms) formErrors.agreedTerms = 'Please, accept our terms'; + else delete formErrors.agreedTerms; + // has CV file ready for upload + if (!formData.fileCV) formErrors.fileCV = 'Please, pick your CV file for uploading'; + else { + const sizeInMB = (formData.fileCV.size / (1024 * 1024)).toFixed(2); + if (sizeInMB > 8) { + formErrors.fileCV = 'Max file size is limited to 8 MB'; + delete formData.fileCV; + } else if (_.endsWith(formData.fileCV.name, '.pdf') || _.endsWith(formData.fileCV.name, '.docx')) { + delete formErrors.fileCV; + } else { + formErrors.fileCV = 'Only .pdf and .docx files are allowed'; + delete formData.fileCV; + } + } + // update errors state + this.setState({ + formErrors, + formData, + }); + } + + render() { + const { formErrors, formData } = this.state; + return ( + + ); + } +} + +RecruitCRMJobApplyContainer.defaultProps = { + user: null, +}; + +RecruitCRMJobApplyContainer.propTypes = { + job: PT.shape().isRequired, + user: PT.shape(), +}; + +function mapStateToProps(state) { + const { profile } = state.auth; + let userData = null; + if (profile && profile.email) { + userData = { + fname: profile.firstName, + lname: profile.lastName, + email: profile.email, + city: profile.addresses && profile.addresses[0] ? profile.addresses[0].city : '', + handle: profile.handle, + }; + } + return { + user: userData, + }; +} + +function mapDispatchToActions() { + // const a = actions.recruit; + // return { + // getJob: (id) => { + // dispatch(a.getJobInit(id)); + // dispatch(a.getJobDone(id)); + // }, + // }; +} + +export default connect( + mapStateToProps, + mapDispatchToActions, +)(RecruitCRMJobApplyContainer); diff --git a/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx index b69aae2efe..7479afa0fe 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx @@ -3,34 +3,40 @@ * driven by recruitCRM */ +import _ from 'lodash'; import actions from 'actions/recruitCRM'; import LoadingIndicator from 'components/LoadingIndicator'; import GigDetails from 'components/Gigs/GigDetails'; import PT from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; +import RecruitCRMJobApply from './RecruitCRMJobApply'; class RecruitCRMJobDetailsContainer extends React.Component { componentDidMount() { const { getJob, id, + job, } = this.props; - getJob(id); + if (_.isEmpty(job)) { + getJob(id); + } } render() { const { loading, job, + isApply, } = this.props; if (loading) { return ; } - return ; + return isApply ? : ; } } @@ -43,6 +49,7 @@ RecruitCRMJobDetailsContainer.propTypes = { loading: PT.bool.isRequired, job: PT.shape(), id: PT.string.isRequired, + isApply: PT.bool.isRequired, }; function mapStateToProps(state, ownProps) { diff --git a/src/shared/containers/Gigs/techSkills.js b/src/shared/containers/Gigs/techSkills.js new file mode 100644 index 0000000000..e9983401b8 --- /dev/null +++ b/src/shared/containers/Gigs/techSkills.js @@ -0,0 +1 @@ +export default ['J2EE', 'Java', 'JavaBean', 'EJB', 'JSP', 'Servlet', 'Applet', 'Java Application', 'JMS', 'Web Services', '.NET', 'VB', 'C++', 'COM', 'XML', 'XSL', 'HTML', 'HTTP', 'C#', 'VB.NET', 'JSF', 'J2ME', 'MIDP 2.0', 'XUL', 'JavaScript', 'IIS', 'Oracle 10g', 'Oracle 9i', 'SQL Server', 'COM+', 'Windows Workflow Foundation', 'Windows Communication Foundation', 'XAML', 'Microsoft SilverLight', 'Spring', 'Dojo', 'AJAX', 'Struts', 'CSS', 'WPF', 'ClickOnce', 'PostgreSQL', '.NET System.Addins', 'MSMQ', 'SWT', 'Eclipse Plugin', 'JSON', 'JUnit', 'Swing', 'JPA', 'Hibernate', 'LDAP', 'Active Directory', 'C', 'MySQL', 'Apache Derby', 'JBoss Seam', 'Fortran', 'PHP', 'Ruby', 'Objective C', 'Ruby on Rails', 'Python', 'UML', 'Word/Rich Text', 'SQL', 'PL/SQL', 'SSIS', 'Windows Server', 'Sharepoint 3.0', 'COBOL', 'IBM WebSphere DataPower', 'Perl', 'SAP', 'Siebel', 'Xcode', 'Blackberry SDK', 'BizTalk', 'Sencha Touch 2', 'HTML5', 'PhoneGap', 'Android', 'Node.js', 'Drools', 'Salesforce', 'iBATIS/MyBatis', 'Apex', 'Visualforce', 'jQuery', 'Angular.js (1.0)', 'MongoDB', 'Twitter Bootstrap', 'API', 'Redis', 'Backbone.js', 'Google App Engine', 'Google API', 'Chatter', 'CoffeeScript', 'iOS', 'Databasedotcom', 'Elasticsearch', 'Express', 'Force.com Sites', 'Go', 'Groovy', 'NoSQL', 'Photoshop', 'Play! Framework', 'REST', 'SFDC Mobile', 'OSX', 'Bootstrap', 'Other', 'HPE Haven OnDemand', 'jQuery Mobile', 'Titanium', 'Swift', 'XMPP', 'Jabber', 'Cisco', 'Data Science', 'Lightning', 'R', 'Vertica', 'tvOS', 'Ant', 'Maven', 'Gradle', 'Spark', 'Docker', 'ReactJS', 'Grommet', 'Matlab', 'Predix', 'Ionic', 'IBM Cognitive', 'IBM Watson', 'AI', 'Cognitive', 'Angular 2+', 'Blockchain', 'ASP.NET Core', 'Continuous Integration', 'AWS', 'Beanstalk', 'Box', 'Brivo Labs', 'Actian X – Hybrid Database', 'Cloud Foundry', 'CloudFactor', 'DocuSign', 'Facebook', 'FinancialForce', 'Actian DataConnect – Data Integration', 'Gaming', 'Google', 'Heroku', 'Actian Vector – SMP Analytics Database', 'Actian Versant – NoSQL Object Database', 'IBM Cloud', 'Actian Ingres – Relational Database', 'Linux', 'MESH01', 'Microsoft Azure', 'Mobile', 'Actian OpenRoad - 4GL', 'Actian PSQL – Embeddable Database', 'Actian Data Management', 'Salesforce.com', 'Smartsheet', 'Twilio', 'Wordpress', 'Hyperledger Fabric', 'Actian Data Integration', 'Actian Data Analytics', 'QA', 'Marvel - Design', 'Ethereum', 'Solidity', 'Xamarin', 'MarkLogic', 'UI Prototype', 'Frontend', 'Quorum', 'R3 Corda', 'Hashgraph', 'Hyperledger Sawtooth', 'Multichain', 'ipfs', 'ActionScript', 'ADO.NET', 'Advanced Math', 'Akka', 'Algorithm', 'Apache Camel', 'Apache Cordova', 'Apache Flume', 'Apache Kafka', 'Appcelerator', 'Appium', 'Apple', 'Apple HIG', 'Atom', 'Axure', 'Bash', 'BigQuery', 'Bower', 'Branding', 'Brute Force', 'Calabash', 'CartoDB', 'Cassandra', 'Castor', 'Chrome', 'Class', 'Clojure', 'Cocoa', 'Codeigniter', 'Commerce Server 2009', 'Compression', 'Concept Design', 'Couchbase', 'Custom Tag', 'Customer Experience (Cx)', 'D3.JS', 'Database', 'Dc.js', 'Django', 'Doctrine', 'Dropwizard', 'Drupal', 'Dynamic Programming', 'EC2', 'Ember.js', 'Entity-Framework', 'Espruino', 'Excel', 'F#', 'File', 'Flash', 'Flex', 'Flight.js', 'Flux', 'Forms', 'Foundation', 'Function', 'Geometry', 'Gimp', 'Git', 'Github', 'Gitlab', 'Google Dart', 'Google-Maps', 'Graph Theory', 'Graphic Design', 'Greedy', 'Gremlin', 'Grunt.js', 'Gulp', 'Hadoop', 'HAML', 'Haskell', 'IBM AiX', 'IBM COGNOS', 'IBM DB2', 'IBM Design', 'IBM Lotus Domino', 'IBM Lotus Notes', 'IBM Pl/1', 'IBM Rational Application Developer', 'IBM Rational Data Architect', 'IBM Rational Software Architect', 'IBM Rational Team Concert', 'IBM REXX', 'IBM Websphere Application Server', 'IBM WebSphere DataStage', 'IBM Websphere Message Broker', 'IBM WebSphere MQ', 'IBM WebSphere Portal', 'IDOL OnDemand', 'Illustrator', 'Image', 'InDesign', 'IndexedDB', 'Infographic', 'Information Architecture (IA)', 'Inkscape', 'Interaction Design (Ixd)', 'InVision', 'JDBC', 'Jekyll', 'JetBrains', 'Jface', 'Jruby', 'Knockout', 'Kraken.js', 'Laravel', 'Leaflet.js', 'Less', 'Linq', 'List', 'Logo', 'Lua', 'Machine Learning', 'MariaDB', 'Math', 'Meteor.js', 'Mobile Design', 'Moodle', 'Mozilla', 'Multithreading', 'Neo4J', 'NGINX', 'Nodewebkit', 'Npm', 'Oauth', 'Om', 'OmniGraffle', 'Openam', 'Openstack', 'OSGi', 'Performance', 'Phantomjs', 'PowerShell', 'Print', 'Product Design', 'Protractor', 'Q & Bluebird', 'Qt', 'Recursion', 'Redhat', 'Regex', 'Remoting', 'Require.js', 'Responsive Design', 'Responsive Web Design', 'RMI', 'S3', 'safari', 'SASS', 'Scala', 'SCSS', 'Search', 'Security', 'Shell', 'Simple Math', 'Simple Search', 'Simulation', 'Sinatra', 'Sketch', 'Sockets', 'Sorting', 'Sqlite', 'String', 'String Manipulation', 'String Parsing', 'Swagger', 'Sympfony', 'Transact-SQL', 'Travis', 'Trello', 'TypeScript', 'Ubuntu', 'UITableView', 'User Experience (Ux)', 'User Interface (Ui)', 'Validation', 'VBA', 'Vert.X', 'vim', 'Visual-Studio', 'WCF', 'Website Design', 'Windows', 'Windows Phone', 'Winforms', 'Winforms Controls', 'Yii', 'Zend framework', 'Zepto.js', 'Zipkin', 'MapReduce', 'Big data', 'Hadoop Admin', 'Tableau', 'Ab Initio', 'Power BI', 'Informatica Big Data', 'Informatica IDQ / IDE', 'QlikView', 'Talend', 'BackOffice Associates', 'Webmethods', 'TIBCO', 'IBM integration', 'Dell Boomi', 'Angular 4', 'Microservices', 'Apigee', 'ASP.NET MVC 5', 'ASP.NET Web API', 'CA', 'DevOps', 'Google Cloud', 'LoadRunner', 'Api Connect', 'Integrator', 'IBM Cloud Private', 'IBM Integration Bus', 'Websphere Liberty', 'JMeter', 'Kubernetes', 'Microsoft Dynamics', 'Office365', 'Microsoft Exchange', 'Open Source', 'OpenShift', 'Selenium', 'Sharepoint 2016', 'Aris', 'Cumulocity', 'Tosca', 'Business-to-business', 'FuseESB', 'WSO2 ESB', 'Mainframe', 'JBehave', 'Splunk', 'Oracle EBS', 'Hybris', 'Pega', 'Relational Algebra', 'Outliers', 'Smoothing', 'Computer Vision', 'Object Detection', 'Pattern Recognition', 'Pattern Matching', 'Video Tracking', 'Pose Estimation', 'NLP', 'Speech Recognition', 'NLG', 'DSS', 'Teaching systems', 'Deep Learning', 'Robotics', 'Recommendation System', 'IOT', 'Expert Systems', 'Genetic Algorithm', 'Evolutionary Algorithm', 'Image Processing', 'OCR', 'Event Detection', '3D Reconstruction', 'Gaussian Filtering', 'Median Filtering', 'Imputation', 'Random Sampling', 'Montecarlo', 'Statistical distributions', 'Regression', 'NLU', 'Dimensionality Reduction', 'Factor Analysis', 'K Means', 'Canopy Clustering', 'TF IDR', 'Feature Hashing', 'Wrapper Methods', 'Sensitivity Analysis', 'Self organizing Maps', 'Deduplication', 'Normalization', 'Format Conversion', 'FFT', 'DWT', 'Coordinate Transformation', 'Statistical Count', 'Standard Deviation', 'Mean', 'Range', 'Boxplot', 'Scatter Plots', 'Distributive Aggregation', 'Distribution fitting', 'Baseball Card Aggregation', 'Enrichment', 'Clustering', 'Baseball Card Aggregation', 'Enrichment', 'Annotation', 'Clustering', 'X Means', 'Canopy', 'Apriori', 'Topic Modeling', 'Non-Elliptical Clustering', 'Fractals', 'Database Scan', 'Guassian Mixture Model', 'K-Means', 'Tree', 'GLM', 'Regression', 'LASSO', 'Elastic Net', 'Stepwise regression', 'T Testing', 'ANOVA', 'Classification', 'Bayesian Network', 'Neural Network', 'Random Forest', 'Decision Tree', 'Nearest Neighbors', 'Naivebayes', 'Hidden Markov Model', 'SVM', 'Nearest Neighbors', 'Collaborative filtering', 'Content-based methods', 'Graph-based methods', 'Logical reasoning', 'Optimization', 'Stochastic', 'Stimulated Annealing', 'Gradient Dissent', 'Linear Programming', 'Integer Programming', 'Non-Linear Optimization', 'Markov Models', 'Agent Based Modelling', 'Montecarlo', 'System Dynamics', 'Activity Based Simulation', 'ODE', 'PDE', 'Fuzzy Logic', 'Deep Fake', 'Algorithimic Trading', 'Clinical Trials', 'Medical Records', 'Diagnostics', 'Angular 8', 'React Native', 'Flutter testing', 'Dummy Challenge', 'Domino', 'Bamboo', 'Jenkins', 'Puppet', 'Ansible', 'Terraform', 'Landslide', 'XSLT', 'BizTalk', 'Three.js', 'Unity 3D', 'CI/CD', 'Databricks', 'Angular', 'Azure', 'Spring Boot', 'Devops', 'Kotlin']; diff --git a/src/shared/containers/GigsPages.jsx b/src/shared/containers/GigsPages.jsx index f0406e7259..1822db9f03 100644 --- a/src/shared/containers/GigsPages.jsx +++ b/src/shared/containers/GigsPages.jsx @@ -13,14 +13,19 @@ import RecruitCRMJobDetails from 'containers/Gigs/RecruitCRMJobDetails'; export default function GigsPagesContainer(props) { const { match } = props; const { id } = match.params; + const isApply = `${config.GIGS_PAGES_PATH}/${id}/apply` === match.url; return (
- +
{ id ? ( ) : ( + ( From 1b0a5fae0c45b86eb4c01a6113e1c42c650840e8 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 23 Sep 2020 11:06:02 +0300 Subject: [PATCH 02/25] Implements #4916 --- src/shared/containers/GigsPages.jsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/shared/containers/GigsPages.jsx b/src/shared/containers/GigsPages.jsx index f0406e7259..ac098eef89 100644 --- a/src/shared/containers/GigsPages.jsx +++ b/src/shared/containers/GigsPages.jsx @@ -6,8 +6,9 @@ import PT from 'prop-types'; import Header from 'containers/TopcoderHeader'; import Footer from 'components/TopcoderFooter'; import Viewport from 'components/Contentful/Viewport'; -import { config, MetaTags } from 'topcoder-react-utils'; +import { config } from 'topcoder-react-utils'; import RecruitCRMJobDetails from 'containers/Gigs/RecruitCRMJobDetails'; +import { Helmet } from 'react-helmet'; export default function GigsPagesContainer(props) { @@ -15,7 +16,17 @@ export default function GigsPagesContainer(props) { const { id } = match.params; return (
- + + Topcoder - Gig Work Opportunities + +
{ id ? ( From d8603af5934ce8bf82dc46db084eb78fcf2ecc66 Mon Sep 17 00:00:00 2001 From: Luiz Ricardo Rodrigues Date: Thu, 24 Sep 2020 01:10:27 -0300 Subject: [PATCH 03/25] ci: Deploy gig-pages-chatbot to QA env Issue: #4916 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index ff01c09e72..bcd6f84b21 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -245,7 +245,7 @@ workflows: filters: branches: only: - - community-app-tests-poc + - gig-pages-chatbot - develop # This is beta env for production soft releases - "build-prod-beta": From 4de51d2fb2aca4a7d6da328e4eb9a6dc4e9b3ac1 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Fri, 25 Sep 2020 15:19:19 +0300 Subject: [PATCH 04/25] Connect to Recruit CRM --- .../Checkbox/__snapshots__/index.jsx.snap | 9 +- package-lock.json | 111 ++++- package.json | 2 + src/assets/images/big-checkmark.png | Bin 0 -> 1186 bytes src/assets/images/sad-face-icon.svg | 20 + src/server/routes/recruitCRM.js | 14 +- src/server/services/recruitCRM.js | 142 ++++++ src/shared/actions/recruitCRM.js | 59 +++ .../GUIKit/Assets/Images/checkmark-large.png | Bin 0 -> 339 bytes .../GUIKit/Assets/Images/checkmark-medium.png | Bin 0 -> 295 bytes .../GUIKit/Assets/Images/checkmark-small.png | Bin 0 -> 280 bytes .../components/GUIKit/Checkbox/index.jsx | 12 +- .../components/GUIKit/Checkbox/style.scss | 20 +- .../components/GUIKit/Datepicker/index.jsx | 2 +- .../components/GUIKit/TextInput/index.jsx | 2 +- .../components/GUIKit/TextInput/style.scss | 1 + src/shared/components/Gigs/GigApply/index.jsx | 430 ++++++++++-------- .../components/Gigs/GigApply/style.scss | 23 + .../containers/Gigs/RecruitCRMJobApply.jsx | 141 +++--- src/shared/containers/Gigs/RecruitCRMJobs.jsx | 10 +- .../containers/Gigs/jobLisingStyles.scss | 8 + src/shared/reducers/recruitCRM.js | 28 ++ src/shared/services/recruitCRM.js | 26 +- 23 files changed, 768 insertions(+), 292 deletions(-) create mode 100644 src/assets/images/big-checkmark.png create mode 100644 src/assets/images/sad-face-icon.svg create mode 100644 src/shared/components/GUIKit/Assets/Images/checkmark-large.png create mode 100644 src/shared/components/GUIKit/Assets/Images/checkmark-medium.png create mode 100644 src/shared/components/GUIKit/Assets/Images/checkmark-small.png diff --git a/__tests__/shared/components/GUIKit/Checkbox/__snapshots__/index.jsx.snap b/__tests__/shared/components/GUIKit/Checkbox/__snapshots__/index.jsx.snap index 94a5da518a..2ca9d09aa5 100644 --- a/__tests__/shared/components/GUIKit/Checkbox/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/GUIKit/Checkbox/__snapshots__/index.jsx.snap @@ -12,13 +12,10 @@ exports[`Default render 1`] = `
-
diff --git a/package-lock.json b/package-lock.json index 5118e651b4..3c35321344 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1934,6 +1934,11 @@ "lodash.throttle": "^4.0.1" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha1-HjRA6RXwsSA9I3SOeO3XubW0PlY=" + }, "append-transform": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/append-transform/-/append-transform-0.4.0.tgz", @@ -3908,6 +3913,38 @@ "safe-json-stringify": "~1" } }, + "busboy": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-0.2.14.tgz", + "integrity": "sha1-bCpiLvz0fFe7vh4qnDetNseSVFM=", + "requires": { + "dicer": "0.2.5", + "readable-stream": "1.1.x" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -4747,7 +4784,6 @@ "version": "1.6.2", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, "requires": { "buffer-from": "^1.0.0", "inherits": "^2.0.3", @@ -6713,6 +6749,38 @@ "defined": "^1.0.0" } }, + "dicer": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/dicer/-/dicer-0.2.5.tgz", + "integrity": "sha1-WZbAhrszIYyBLAkL3cCc0S+stw8=", + "requires": { + "readable-stream": "1.1.x", + "streamsearch": "0.1.2" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + }, + "readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=" + } + } + }, "diff": { "version": "3.5.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", @@ -9016,12 +9084,12 @@ "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=" }, "form-data": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", - "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz", + "integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==", "requires": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, @@ -14499,6 +14567,21 @@ "isarray": "^1.0.0" } }, + "multer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.2.tgz", + "integrity": "sha512-xY8pX7V+ybyUpbYMxtjM9KAiD9ixtg5/JkeKUTD6xilfDv0vzzOFcCp4Ljb1UU3tSOM3VTZtKo63OmzOrGi3Cg==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^0.2.11", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.1", + "object-assign": "^4.1.1", + "on-finished": "^2.3.0", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", @@ -21164,6 +21247,11 @@ "integrity": "sha512-DBp0lSvX5G9KGRDTkR/R+a29H+Wk2xItOF+MpZLLNDWbEV9tGPnqLPxHEYjmiz8xGtJHRIqmI+hCjmNzqoA4nQ==", "dev": true }, + "streamsearch": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-0.1.2.tgz", + "integrity": "sha1-gIudDlb8Jz2Am6VzOOkpkZoanxo=" + }, "strict-uri-encode": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", @@ -22118,6 +22206,16 @@ "ms": "^2.1.1" } }, + "form-data": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.1.tgz", + "integrity": "sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + } + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -33626,8 +33724,7 @@ "typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", - "dev": true + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" }, "typical": { "version": "4.0.0", diff --git a/package.json b/package.json index 1fb8f23e5e..b811711b8a 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "filestack-react": "^2.0.0", "flag-icon-css": "^3.3.0", "focus-trap-react": "^6.0.0", + "form-data": "^3.0.0", "helmet": "^3.12.1", "highlight.js": "^9.18.1", "html-to-text": "^5.1.1", @@ -81,6 +82,7 @@ "moment-timezone": "^0.5.21", "money": "^0.2.0", "morgan": "^1.9.0", + "multer": "^1.4.2", "navigation-component": "topcoder-platform/navigation-component#develop", "node-forge": "^0.7.5", "nuka-carousel": "^4.5.3", diff --git a/src/assets/images/big-checkmark.png b/src/assets/images/big-checkmark.png new file mode 100644 index 0000000000000000000000000000000000000000..8bd83bc41e11a1aaf6e5254f41b1ddc430eaa319 GIT binary patch literal 1186 zcmXAn2~ZPP7{>z)hsiE+sx7BrJ%Dg1hg3lBI~7AjARr=m5)cYZP@_~RD5UN#K`}6N zoJzS$i$IvMV6q92sid{3BeqDzO1P3pQ7T6`qEOPeF!Swu-}{c=_kVBpCX*2bldZ`l z5(%b9hsS|E4SdmNMqq6Td8r1Q0Vgghl+^Lmb{hBv%$WEH5Zv>s3KC-!F+vfcU zQ4legRFK68S%i?q==T0^VH_pm5)2$c86-j=oDT{>7bu2s;1C9hK^I1F#E8pKD+G!O z!zefgvYYi32^a?=2n7cyWW&cOQj{Rz6`Ls#2fYMb1c1Q6=uH$##y9yWEagKu3gH+m z!5}dLZ)gBYU~vT`0$CtzQyxl87bE5yKywkF7$HW@r5IpRL_~^;^NH!807u9@m0&L6 zgX>Fxo{iuJ0mz_;DhR0(J|PdN>g7XHf&fW~E*Ox(+ItJ=hou#pSKf}_@p|$N!4aW) zy*}bv(jQlz^lk9bXK^3ej|HtZ#3#Iwvd*t5O_G+qlA~9S zN2Nrb_%9+g{A5^K`cmkrkkjkIXBN+%(*|U){WA|OT=2{G&6)oqckW=`XZf?ER*qNo8Fx zrcwr(mcLAxvK2m9_tu@vNLt~|Qm2NmqK$1uE2GHgN^RZ2toEiWRrkK1^|j}Wa`b<{ zRq(b27Y#lCOWwx4cHQ8^N>+AUF^hw4=^cvGC*Cay-u2_UP~bDxTgPe*tBoDDJ9$yD zkh)ij+|^X?{q6E9S5vD=sI(nB%&o3bt(uJm?UYkJ1HJahGpur98%=YKCcqg z-I{c4u?%vP54Z=OsjaJ?q%w;bND#Xws9*Qqn6~b&v1r!qE`Q&MrBzKb5k5)72h!%O)tiL8`Hd7T5U$T6{JXYPaKHr_fn zMJ{hHNA7XI%eA+!+&1ZjHv{ z*?{47zcsh^0bV^sIUC^huu&`Poc-7t9;shT4H$*7P8mC2Z>O5Qpp{v|n8hV~=6Zb~ zHKtScTwT`_B`7|zg&H#`!z9`p^RX^!C8-b{k$EYCgaXs^nL8uwA}33HKS_64-bEJf z_*!8^(OOHjp3YR)8mf=5L2F&|hMP|F?_9vi;drptjAy8UTK=421MW*MIP e-K!6*)fNWL*bgMy;BoMuljsqQ@Q%>*(*FVE3M`la literal 0 HcmV?d00001 diff --git a/src/assets/images/sad-face-icon.svg b/src/assets/images/sad-face-icon.svg new file mode 100644 index 0000000000..b9b685c036 --- /dev/null +++ b/src/assets/images/sad-face-icon.svg @@ -0,0 +1,20 @@ + + + + 838C3DF4-2CF1-42A3-BC49-21142AF2003C@2x + Created with sketchtool. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server/routes/recruitCRM.js b/src/server/routes/recruitCRM.js index 5047dcdb0d..34c1831539 100644 --- a/src/server/routes/recruitCRM.js +++ b/src/server/routes/recruitCRM.js @@ -6,7 +6,15 @@ import express from 'express'; import RecruitCRMService from '../services/recruitCRM'; const cors = require('cors'); - +const multer = require('multer'); + +const storage = multer.memoryStorage(); +const upload = multer({ + storage, + limits: { + fileSize: 8000000, + }, +}); const routes = express.Router(); // Enables CORS on those routes according config above @@ -15,9 +23,9 @@ routes.use(cors()); routes.options('*', cors()); routes.get('/jobs', (req, res, next) => new RecruitCRMService().getAllJobs(req, res, next)); - routes.get('/jobs/search', (req, res, next) => new RecruitCRMService().getJobs(req, res, next)); - routes.get('/jobs/:id', (req, res, next) => new RecruitCRMService().getJob(req, res, next)); +routes.post('/jobs/:id/apply', upload.single('resume'), (req, res, next) => new RecruitCRMService().applyForJob(req, res, next)); +routes.get('/candidates/search', (req, res, next) => new RecruitCRMService().searchCandidates(req, res, next)); export default routes; diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js index b8659ae824..6fe82187f6 100644 --- a/src/server/services/recruitCRM.js +++ b/src/server/services/recruitCRM.js @@ -6,6 +6,8 @@ import config from 'config'; import qs from 'qs'; import _ from 'lodash'; +const FormData = require('form-data'); + /** * Auxiliary class that handles communication with recruitCRM */ @@ -141,4 +143,144 @@ export default class RecruitCRMService { return next(err); } } + + /** + * Search for candidate by email endpoint. + * @return {Promise} + * @param {Object} the request. + */ + async searchCandidates(req, res, next) { + try { + const response = await fetch(`${this.private.baseUrl}/v1/candidates/search?${qs.stringify(req.query)}`, { + method: 'GET', + headers: { + 'Content-Type': req.headers['content-type'], + Authorization: this.private.authorization, + }, + }); + if (response.status === 429) { + await new Promise(resolve => setTimeout(resolve, 30000)); // wait 30sec + return this.searchCandidates(req, res, next); + } + if (response.status >= 400) { + return res.send({ + error: true, + status: response.status, + url: `${this.private.baseUrl}/v1/candidates/search?${qs.stringify(req.query)}`, + }); + } + const data = await response.json(); + return res.send(data); + } catch (err) { + return next(err); + } + } + + /** + * Apply for candidate for job endpoint. + * @return {Promise} + * @param {Object} the request. + */ + async applyForJob(req, res, next) { + const { id } = req.params; + const { body, file } = req; + const form = JSON.parse(body.form); + const fileData = new FormData(); + fileData.append('resume', file.buffer, file.originalname); + let candidateSlug; + try { + // Check if candidate exsits in the system? + const candidateResponse = await fetch(`${this.private.baseUrl}/v1/candidates/search?email=${form.email}`, { + method: 'GET', + headers: { + 'Content-Type': req.headers['content-type'], + Authorization: this.private.authorization, + }, + }); + if (candidateResponse.status >= 400) { + return res.send({ + error: true, + status: candidateResponse.status, + url: `${this.private.baseUrl}/v1/candidates/search?email=${form.email}`, + errObj: await candidateResponse.json(), + }); + } + let candidateData = await candidateResponse.json(); + if (candidateData.data) { + // Candidate exists we will update profile fields + // otherwise we create it + candidateSlug = candidateData.data[0].slug; + } + // Create/update candidate profile + const workCandidateResponse = await fetch(`${this.private.baseUrl}/v1/candidates${candidateSlug ? `/${candidateSlug}` : ''}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: this.private.authorization, + }, + body: body.form, + }); + if (workCandidateResponse.status >= 400) { + return res.send({ + error: true, + status: workCandidateResponse.status, + url: `${this.private.baseUrl}/v1/candidates${candidateSlug ? `/${candidateSlug}` : ''}`, + form, + errObj: await workCandidateResponse.json(), + }); + } + candidateData = await workCandidateResponse.json(); + // Attach resume to candidate + const formHeaders = fileData.getHeaders(); + const fileCandidateResponse = await fetch(`${this.private.baseUrl}/v1/candidates/${candidateData.slug}`, { + method: 'POST', + headers: { + Authorization: this.private.authorization, + ...formHeaders, + }, + body: fileData, + }); + if (fileCandidateResponse.status >= 400) { + return res.send({ + error: true, + status: fileCandidateResponse.status, + url: `${this.private.baseUrl}/v1/candidates/${candidateData.slug}`, + form, + fileData, + file, + formHeaders, + errObj: await fileCandidateResponse.json(), + }); + } + candidateData = await fileCandidateResponse.json(); + // Candidate ready to apply for job + const applyResponse = await fetch(`${this.private.baseUrl}/v1/candidates/${candidateData.slug}/assign?job_slug=${id}`, { + method: 'POST', + headers: { + 'Content-Type': req.headers['content-type'], + Authorization: this.private.authorization, + }, + }); + if (applyResponse.status >= 400) { + const errObj = await applyResponse.json(); + if (errObj.errorCode === 422 && errObj.errorMessage === 'Candidate is already assigned to this job') { + return res.send({ + success: true, + }); + } + return res.send({ + error: true, + status: applyResponse.status, + url: `${this.private.baseUrl}/v1/candidates/${candidateData.slug}/assign?job_slug=${id}`, + form, + candidateData, + errObj, + }); + } + const data = await applyResponse.json(); + return res.send(data); + } catch (err) { + return next(err); + } + } } diff --git a/src/shared/actions/recruitCRM.js b/src/shared/actions/recruitCRM.js index 670b71878a..9e153d787f 100644 --- a/src/shared/actions/recruitCRM.js +++ b/src/shared/actions/recruitCRM.js @@ -40,11 +40,70 @@ async function getJobDone(id) { }; } +/** + * Apply for Job init + */ +function applyForJobInit(id, payload) { + return { id, payload }; +} + +/** + * Helper utitlity + * @param {object} payload The apply form payload + */ +function normalizeRecruitPayload(payload) { + return { + last_name: payload.lname, + first_name: payload.fname, + email: payload.email, + contact_number: payload.phone, + city: payload.city, + locality: payload.country, + available_from: payload.availFrom, + salary_expectation: payload.payExpectation, + skill: payload.skills.filter(s => s.selected).map(s => s.label).join(','), + custom_fields: [ + { + field_id: 13, + value: payload.reffereal || '', + }, + { + field_id: 1, + value: payload.tcProfileLink || (payload.handle ? `topcoder.com/members/${payload.handle}` : ''), + }, + { + field_id: 2, + value: payload.handle || '', + }, + { + field_id: 3, + value: payload.whyFit || '', + }, + ], + resume: payload.fileCV, + }; +} + +/** + * Apply for Job done + */ +async function applyForJobDone(id, payload) { + const ss = new Service(); + const res = await ss.applyForJob(id, normalizeRecruitPayload(payload)); + + return { + id, + data: res, + }; +} + export default redux.createActions({ RECRUIT: { GET_JOBS_INIT: getJobsInit, GET_JOBS_DONE: getJobsDone, GET_JOB_INIT: getJobInit, GET_JOB_DONE: getJobDone, + APPLY_FOR_JOB_INIT: applyForJobInit, + APPLY_FOR_JOB_DONE: applyForJobDone, }, }); diff --git a/src/shared/components/GUIKit/Assets/Images/checkmark-large.png b/src/shared/components/GUIKit/Assets/Images/checkmark-large.png new file mode 100644 index 0000000000000000000000000000000000000000..d3c90d8c6e41f537173fecd095ecd66d7b84fe38 GIT binary patch literal 339 zcmV-Z0j&OsP)4u*Y00001b5ch_0Itp) z=>Px$4M{{nR5%fx(!mOXKo9^>tsv^AWOWXrF3}eRU7|V!{!h>kbns97kGy!YPS(j& zi6V$7sLi--XlS?&th?aO+nHrKu0eR7H;Ci-n5OBhL3)tGb2?PR4mj`NUZc@01}kMO?lACe?dVb>z}gCd1? za!4Qceyiav%h~`C;ZcCS;4+uE0WbFVPO1<*yshi{84((Y*|xm}q^g}lC5T{jBuQGC zrg@=I+5E2(!jK+^f*|axusR6EIPx#;Ymb6R45f=U?3ha{`~nf^v|C^NyKY`sAT%^;X~DbDERg3S2D2%Fui;Ct{SEh zL_d4>EC#5Vk&pox8V?*eFcYYW2d;V5s#VOtfB&xi`}c1yT#$hQL*u@E`(^>v-5)5rpphdY^N!GZ;OK+S()1^@%(2#gJ)k!(cK2(lXF1j9v( z7EJ`IgBt+Tj87v-Gsp>`05C^4fRILzW)MIz;NQQ0e~Hxy(T*^{EF&WWl!`#1is46? tVr(?X1)M-E0K_C|L^cc_7080Px#(n&-?R2UhBj=KuNFcgNz!(|H)yjqtY~;m1TKUN{u>HW^m4zMN!-#t1QdTZ2=<)f>oa950deV z@O^)cfKi8{D4O8$gtsYOe#=Rcygbj_;=W(YSWF?rg*sJLb&KP8qdSxAy6z6n0DQXR zU^$Mnmr_0{q^n7W&rC44ZTmnH1=?f^P| cb(q), config.GUIKIT.DEBOUNCE_ON_CHANGE_TIME), @@ -39,7 +41,7 @@ function Checkbox({ }} />
- + checkmark-icon
{errorMsg ? ({errorMsg}) : null} diff --git a/src/shared/components/GUIKit/Checkbox/style.scss b/src/shared/components/GUIKit/Checkbox/style.scss index 1be1148f92..28da3d2e24 100644 --- a/src/shared/components/GUIKit/Checkbox/style.scss +++ b/src/shared/components/GUIKit/Checkbox/style.scss @@ -18,14 +18,6 @@ display: none; left: 50%; top: 50%; - -webkit-filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.35)); - filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.35)); - - :global { - path { - fill: $tc-white; - } - } } } @@ -50,8 +42,8 @@ border-radius: 4px; .after { - margin-left: -8px; - margin-top: -9px; + margin-left: -9px; + margin-top: -7px; } } } @@ -67,8 +59,8 @@ border-radius: 3px; .after { - margin-left: -6px; - margin-top: -7px; + margin-left: -7px; + margin-top: -6px; } } } @@ -84,8 +76,8 @@ border-radius: 2px; .after { - margin-left: -5px; - margin-top: -5px; + margin-left: -6px; + margin-top: -4px; } } } diff --git a/src/shared/components/GUIKit/Datepicker/index.jsx b/src/shared/components/GUIKit/Datepicker/index.jsx index 5cf376d7ef..c67f50de7c 100644 --- a/src/shared/components/GUIKit/Datepicker/index.jsx +++ b/src/shared/components/GUIKit/Datepicker/index.jsx @@ -41,7 +41,7 @@ function Datepicker({ date={date} onDateChange={(changedDate) => { setDate(changedDate); - onChange(changedDate ? changedDate.toDate() : null); + onChange(changedDate.toDate()); }} focused={focused} onFocusChange={({ focused: changedFocused }) => setFocused(changedFocused) diff --git a/src/shared/components/GUIKit/TextInput/index.jsx b/src/shared/components/GUIKit/TextInput/index.jsx index 95a86dd516..6194c0d27e 100644 --- a/src/shared/components/GUIKit/TextInput/index.jsx +++ b/src/shared/components/GUIKit/TextInput/index.jsx @@ -30,7 +30,7 @@ function TextInput({ defaultValue={value} type="text" placeholder={`${placeholder}${placeholder && required ? ' *' : ''}`} - styleName={`${val ? 'haveValue' : ''} ${errorMsg ? 'haveError' : ''}`} + styleName={`${value || val ? 'haveValue' : ''} ${errorMsg ? 'haveError' : ''}`} onChange={(e) => { delayedOnChange(e.target.value, onChange); setVal(e.target.value); diff --git a/src/shared/components/GUIKit/TextInput/style.scss b/src/shared/components/GUIKit/TextInput/style.scss index 453370fbb8..356a3215e5 100644 --- a/src/shared/components/GUIKit/TextInput/style.scss +++ b/src/shared/components/GUIKit/TextInput/style.scss @@ -41,6 +41,7 @@ } input:not([type='checkbox']).haveValue + label, + input:not([type='checkbox']).haveError + label, input:not([type='checkbox']):focus + label { display: flex; } diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx index 889c45eb3b..2768e394c1 100644 --- a/src/shared/components/Gigs/GigApply/index.jsx +++ b/src/shared/components/Gigs/GigApply/index.jsx @@ -15,10 +15,12 @@ import { getCustomField } from 'utils/gigs'; import Modal from 'components/Contentful/Modal'; import FilestackFilePicker from 'components/GUIKit/FilePicker'; import './style.scss'; +import bigCheckmark from 'assets/images/big-checkmark.png'; +import SadFace from 'assets/images/sad-face-icon.svg'; export default function GigApply(props) { const { - job, onFormInputChange, formData, formErrors, onApplyClick, + job, onFormInputChange, formData, formErrors, onApplyClick, applying, application, } = props; return ( @@ -36,205 +38,243 @@ export default function GigApply(props) {

{job.name}

GIG DETAILS
-
-

PERSONAL INFORMATION

-

Welcome to Topcoder Gigs! We’d like to get to know you.

-
-
- onFormInputChange('fname', val)} - errorMsg={formErrors.fname} - value={formData.fname} - required - /> - onFormInputChange('lname', val)} - errorMsg={formErrors.lname} - value={formData.lname} - required - /> -
-
- onFormInputChange('email', val)} - errorMsg={formErrors.email} - value={formData.email} - required - /> - onFormInputChange('phone', val)} - errorMsg={formErrors.phone} - value={formData.phone} - required - /> -
-
- onFormInputChange('city', val)} - errorMsg={formErrors.city} - value={formData.city} - required - /> - onFormInputChange('country', val)} - errorMsg={formErrors.country} - value={formData.country} - required - /> -
-
-

TOPCODER INFORMATION

-

If you have a Topcoder profile, please share. Not a Member?

-
-
- onFormInputChange('handle', val)} - errorMsg={formErrors.handle} - value={formData.handle} - /> - onFormInputChange('tcProfileLink', val)} - errorMsg={formErrors.tcProfileLink} - value={formData.handle ? `topcoder.com/members/${formData.handle}` : null} - /> -
-
-

SHARE YOUR EXPECTATIONS

-

Your Professional Work History

-
-
- onFormInputChange('payExpectation', val)} - errorMsg={formErrors.Expectation} - /> - onFormInputChange('availFrom', val)} - errorMsg={formErrors.availFrom} - value={formData.availFrom} - /> -
-
-

RESUME & SKILLS

-

Upload Your Resume or CV

-
- onFormInputChange('fileCV', files[0])} - inputOptions={{ - accept: '.pdf,.docx', - }} - infoText="Drag & drop your resume or CV here - please omit contact information *" - errorMsg={formErrors.fileCV} - /> -
- onFormInputChange('skills', val)} - errorMsg={formErrors.skills} - addNewOptionPlaceholder="Type to add another skill..." - required - /> -
-

FINAL QUESTIONS

-

Please Complete the Following Questions

-
- onFormInputChange('reffereal', val)} - errorMsg={formErrors.reffereal} - value={formData.reffereal} - required - /> -
- onFormInputChange('whyFit', val)} - errorMsg={formErrors.whyFit} - /> -

What is your availability per week?

-
+ { + application ? ( +
+ { application.error ? : bigCheckmark OK } +

{application.error ? 'OOPS!' : 'APPLICATION SUBMITTED'}

{ - _.map(formData.timeAvailability, (cbox, indx) => ( -
- { - formData.timeAvailability[indx].checked = val; - onFormInputChange('timeAvailability', formData.timeAvailability); - }} - checked={formData.timeAvailability[indx].checked} - size="lg" - /> - {cbox.label} -
- )) + application.error ? ( +

Looks like there is a problem on our end. Please try again.
If this persists please contact support@topcoder.com.

+ ) : ( +

We will contact you via email if it seems like a fit!

+ ) } +
+ { + application.error ? ( + { + e.preventDefault(); + window.location.reload(); + }} + >APPLY AGAIN + + ) : ( + GO TO GIG LIST + ) + } +
-

Are you able to work during the specified timezone? ({`${getCustomField(job.custom_fields, 'Timezone')}`})

- onFormInputChange('timezoneConfirm', val)} - errorMsg={formErrors.timezoneConfirm} - options={formData.timezoneConfirm} - size="lg" - /> -

Are you ok to work with the duration of the gig? ({`${getCustomField(job.custom_fields, 'Duration')}`})

- onFormInputChange('durationConfirm', val)} - errorMsg={formErrors.durationConfirm} - options={formData.durationConfirm} - size="lg" - /> -
- onFormInputChange('notes', val)} - errorMsg={formErrors.notes} - /> -
-
-
-
-
-
- onFormInputChange('agreedTerms', val)} - checked={formData.agreedTerms} - errorMsg={formErrors.agreedTerms} + ) : null + } + { + !application ? ( +
+

PERSONAL INFORMATION

+

Welcome to Topcoder Gigs! We’d like to get to know you.

+
+
+ onFormInputChange('fname', val)} + errorMsg={formErrors.fname} + value={formData.fname} + required + /> + onFormInputChange('lname', val)} + errorMsg={formErrors.lname} + value={formData.lname} + required + /> +
+
+ onFormInputChange('email', val)} + errorMsg={formErrors.email} + value={formData.email} + required + /> + onFormInputChange('phone', val)} + errorMsg={formErrors.phone} + value={formData.phone} + required + /> +
+
+ onFormInputChange('city', val)} + errorMsg={formErrors.city} + value={formData.city} + required + /> + onFormInputChange('country', val)} + errorMsg={formErrors.country} + value={formData.country} + required + /> +
+
+

TOPCODER INFORMATION

+

If you have a Topcoder profile, please share. Not a Member?

+
+
+ onFormInputChange('handle', val)} + errorMsg={formErrors.handle} + value={formData.handle} + /> + onFormInputChange('tcProfileLink', val)} + errorMsg={formErrors.tcProfileLink} + value={formData.handle ? `topcoder.com/members/${formData.handle}` : null} + /> +
+
+

SHARE YOUR EXPECTATIONS

+

Your Professional Work History

+
+
+ onFormInputChange('payExpectation', val)} + errorMsg={formErrors.payExpectation} + value={formData.payExpectation} + /> + onFormInputChange('availFrom', val.toISOString())} + errorMsg={formErrors.availFrom} + value={formData.availFrom} + /> +
+
+

RESUME & SKILLS

+

Upload Your Resume or CV

+
+ onFormInputChange('fileCV', files[0])} + inputOptions={{ + accept: '.pdf,.docx', + }} + infoText="Drag & drop your resume or CV here - please omit contact information *" + errorMsg={formErrors.fileCV} + /> +
+ onFormInputChange('skills', val)} + errorMsg={formErrors.skills} + addNewOptionPlaceholder="Type to add another skill..." + required + /> +
+

FINAL QUESTIONS

+

Please Complete the Following Questions

+
+ onFormInputChange('reffereal', val)} + errorMsg={formErrors.reffereal} + value={formData.reffereal} + required + /> +
+ onFormInputChange('whyFit', val)} + errorMsg={formErrors.whyFit} + value={formData.whyFit} + /> +

What is your availability per week?

+
+ { + _.map(formData.timeAvailability, (cbox, indx) => ( +
+ { + formData.timeAvailability[indx].checked = val; + onFormInputChange('timeAvailability', formData.timeAvailability); + }} + checked={formData.timeAvailability[indx].checked} + size="lg" + /> + {cbox.label} +
+ )) + } +
+

Are you able to work during the specified timezone? ({`${getCustomField(job.custom_fields, 'Timezone')}`})

+ onFormInputChange('timezoneConfirm', val)} + errorMsg={formErrors.timezoneConfirm} + options={formData.timezoneConfirm} + size="lg" + /> +

Are you ok to work with the duration of the gig? ({`${getCustomField(job.custom_fields, 'Duration')}`})

+ onFormInputChange('durationConfirm', val)} + errorMsg={formErrors.durationConfirm} + options={formData.durationConfirm} size="lg" /> - I agree to Candidate Terms * +
+ onFormInputChange('notes', val)} + errorMsg={formErrors.notes} + /> +
+
+
+
+
+ onFormInputChange('agreedTerms', val)} + checked={formData.agreedTerms} + errorMsg={formErrors.agreedTerms} + size="lg" + /> + I agree to Candidate Terms * +
+
+ View Our Equal Employment Opportunity Policy +
+
- View Our Equal Employment Opportunity Policy -
- -
+ ) : null + }
) } @@ -244,6 +284,8 @@ export default function GigApply(props) { GigApply.defaultProps = { formErrors: {}, + applying: false, + application: null, }; GigApply.propTypes = { @@ -252,4 +294,6 @@ GigApply.propTypes = { formData: PT.shape().isRequired, onFormInputChange: PT.func.isRequired, onApplyClick: PT.func.isRequired, + applying: PT.bool, + application: PT.shape(), }; diff --git a/src/shared/components/Gigs/GigApply/style.scss b/src/shared/components/Gigs/GigApply/style.scss index fd4829d897..b109208cf7 100644 --- a/src/shared/components/Gigs/GigApply/style.scss +++ b/src/shared/components/Gigs/GigApply/style.scss @@ -68,6 +68,29 @@ border-bottom: 1px solid #e9e9e9; } + .apply-state { + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 162px; + margin-top: 50px; + + h2 { + color: #2a2a2a; + } + + p { + font-size: 24px; + line-height: 36px; + text-align: center; + + a { + font-size: 24px; + line-height: 36px; + } + } + } + .form-wrap { max-width: 880px; margin: auto; diff --git a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx index 9ea798e0d2..b492fa680a 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx @@ -3,7 +3,7 @@ */ import _ from 'lodash'; -// import actions from 'actions/recruitCRM'; +import actions from 'actions/recruitCRM'; import GigApply from 'components/Gigs/GigApply'; import PT from 'prop-types'; import React from 'react'; @@ -17,8 +17,8 @@ class RecruitCRMJobApplyContainer extends React.Component { // initial state this.state = { formErrors: {}, - formData: _.merge({ - availFrom: new Date(), + formData: { + availFrom: new Date().toISOString(), skills: _.map(techSkills, label => ({ label, selected: false })), durationConfirm: [{ label: 'Yes', value: false }, { label: 'No', value: false }], timezoneConfirm: [{ label: 'Yes', value: false }, { label: 'No', value: false }], @@ -26,8 +26,8 @@ class RecruitCRMJobApplyContainer extends React.Component { { label: '10 hours', checked: false }, { label: '20 hours', checked: false }, { label: '30 hours', checked: false }, { label: '40 hours', checked: false }, ], agreedTerms: false, - // eslint-disable-next-line react/destructuring-assignment - }, this.props.user), + // eslint-disable-next-line react/destructuring-assignment + }, }; // binds @@ -37,68 +37,81 @@ class RecruitCRMJobApplyContainer extends React.Component { } componentDidMount() { + const { formData } = this.state; + const { user } = this.props; + this.setState({ + formData: _.merge(formData, user), + }); this.validateForm(); } onFormInputChange(key, value) { - const { formData } = this.state; // update the state - this.setState({ + this.setState(state => ({ + ...state, formData: { - ...formData, + ...state.formData, [key]: value, }, - }); + })); // run form validation this.validateForm(); } - // eslint-disable-next-line class-methods-use-this onApplyClick() { - // eslint-disable-next-line no-console - console.log('onApplyClick'); + const { applyForJob, job } = this.props; + const { formData } = this.state; + applyForJob(job.slug, formData); } validateForm() { - const { formData, formErrors } = this.state; - // Form validation happens here - const requiredTextFields = [ - 'fname', 'lname', 'city', 'country', 'reffereal', 'phone', 'email', - ]; - // check required text fields for value - _.each(requiredTextFields, (key) => { - if (!formData[key] || !_.trim(formData[key])) formErrors[key] = 'Required field'; - else delete formErrors[key]; - }); - // check for valid email - if (formData.email && _.trim(formData.email)) { - if (!(isValidEmail(formData.email))) formErrors.email = 'Invalid email'; - else delete formErrors.email; - } - // require atleast 1 skill - if (!_.find(formData.skills, { selected: true })) formErrors.skills = 'Please, add technical skills'; - else delete formErrors.skills; - // have accepted terms - if (!formData.agreedTerms) formErrors.agreedTerms = 'Please, accept our terms'; - else delete formErrors.agreedTerms; - // has CV file ready for upload - if (!formData.fileCV) formErrors.fileCV = 'Please, pick your CV file for uploading'; - else { - const sizeInMB = (formData.fileCV.size / (1024 * 1024)).toFixed(2); - if (sizeInMB > 8) { - formErrors.fileCV = 'Max file size is limited to 8 MB'; - delete formData.fileCV; - } else if (_.endsWith(formData.fileCV.name, '.pdf') || _.endsWith(formData.fileCV.name, '.docx')) { - delete formErrors.fileCV; - } else { - formErrors.fileCV = 'Only .pdf and .docx files are allowed'; - delete formData.fileCV; + this.setState((state) => { + const { formData, formErrors } = state; + // Form validation happens here + const requiredTextFields = [ + 'fname', 'lname', 'city', 'country', 'reffereal', 'phone', 'email', + ]; + // check required text fields for value + _.each(requiredTextFields, (key) => { + if (!formData[key] || !_.trim(formData[key])) formErrors[key] = 'Required field'; + else if (formData[key] && _.trim(formData[key]).length < 2) formErrors[key] = 'Must be at least 2 characters'; + else delete formErrors[key]; + }); + // check payExpectation to be a number + if (formData.payExpectation && _.trim(formData.payExpectation)) { + if (!_.isInteger(_.toNumber(formData.payExpectation))) formErrors.payExpectation = 'Must be integer value in $'; + else delete formErrors.payExpectation; + } else delete formErrors.payExpectation; + // check for valid email + if (formData.email && _.trim(formData.email)) { + if (!(isValidEmail(formData.email))) formErrors.email = 'Invalid email'; + else delete formErrors.email; } - } - // update errors state - this.setState({ - formErrors, - formData, + // require atleast 1 skill + if (!_.find(formData.skills, { selected: true })) formErrors.skills = 'Please, add technical skills'; + else delete formErrors.skills; + // have accepted terms + if (!formData.agreedTerms) formErrors.agreedTerms = 'Please, accept our terms'; + else delete formErrors.agreedTerms; + // has CV file ready for upload + if (!formData.fileCV) formErrors.fileCV = 'Please, pick your CV file for uploading'; + else { + const sizeInMB = (formData.fileCV.size / (1024 * 1024)).toFixed(2); + if (sizeInMB > 8) { + formErrors.fileCV = 'Max file size is limited to 8 MB'; + delete formData.fileCV; + } else if (_.endsWith(formData.fileCV.name, '.pdf') || _.endsWith(formData.fileCV.name, '.docx')) { + delete formErrors.fileCV; + } else { + formErrors.fileCV = 'Only .pdf and .docx files are allowed'; + delete formErrors.fileCV; + } + } + // updated state + return { + ...state, + formErrors, + }; }); } @@ -118,15 +131,21 @@ class RecruitCRMJobApplyContainer extends React.Component { RecruitCRMJobApplyContainer.defaultProps = { user: null, + applying: false, + application: null, }; RecruitCRMJobApplyContainer.propTypes = { job: PT.shape().isRequired, user: PT.shape(), + applyForJob: PT.func.isRequired, + applying: PT.bool, + application: PT.shape(), }; -function mapStateToProps(state) { +function mapStateToProps(state, ownProps) { const { profile } = state.auth; + const { job } = ownProps; let userData = null; if (profile && profile.email) { userData = { @@ -139,17 +158,21 @@ function mapStateToProps(state) { } return { user: userData, + applying: state.recruitCRM && state.recruitCRM[job.slug] + ? state.recruitCRM[job.slug].applying : false, + application: state.recruitCRM && state.recruitCRM[job.slug] + ? state.recruitCRM[job.slug].application : null, }; } -function mapDispatchToActions() { - // const a = actions.recruit; - // return { - // getJob: (id) => { - // dispatch(a.getJobInit(id)); - // dispatch(a.getJobDone(id)); - // }, - // }; +function mapDispatchToActions(dispatch) { + const a = actions.recruit; + return { + applyForJob: (id, payload) => { + dispatch(a.applyForJobInit(id, payload)); + dispatch(a.applyForJobDone(id, payload)); + }, + }; } export default connect( diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx index 59b2973ba8..a806d1fa07 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobs.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx @@ -109,7 +109,12 @@ class RecruitCRMJobsContainer extends React.Component { } = this.state; if (loading) { - return ; + return ( + + ; +

Searching our database for the best gigs…

+
+ ); } let jobsToDisplay = jobs; @@ -139,6 +144,9 @@ class RecruitCRMJobsContainer extends React.Component { if (skills && skills.value && skills.value.toLowerCase().includes(_term)) return true; // location if (job.country.toLowerCase().includes(_term)) return true; + // duration + const duration = _.find(job.custom_fields, ['field_name', 'Duration']); + if (duration && duration.value && duration.value.toLowerCase().includes(_term)) return true; // no match return false; }); diff --git a/src/shared/containers/Gigs/jobLisingStyles.scss b/src/shared/containers/Gigs/jobLisingStyles.scss index 1d909ee9c1..923c4fe8fb 100644 --- a/src/shared/containers/Gigs/jobLisingStyles.scss +++ b/src/shared/containers/Gigs/jobLisingStyles.scss @@ -1,5 +1,13 @@ @import "~styles/mixins"; +.loading-text { + font-family: Roboto, sans-serif; + font-size: 24px; + line-height: 26px; + color: #2a2a2a; + text-align: center; +} + .container { max-width: $screen-lg; margin: auto; diff --git a/src/shared/reducers/recruitCRM.js b/src/shared/reducers/recruitCRM.js index 599080cc17..786caef018 100644 --- a/src/shared/reducers/recruitCRM.js +++ b/src/shared/reducers/recruitCRM.js @@ -58,6 +58,32 @@ function onJobDone(state, { payload }) { }; } +/** + * Handles recruit.applyForJobInit action. + * @param {Object} state Previous state. + */ +function onApplyForJobInit(state, { payload }) { + const r = { + ...state, + }; + r[payload.id].applying = true; + return r; +} + +/** + * Handles recruit.applyForJobDone action. + * @param {Object} state Previous state. + * @param {Object} action The action. + */ +function onApplyForJobDone(state, { payload }) { + const r = { + ...state, + }; + r[payload.id].applying = false; + r[payload.id].application = payload.data; + return r; +} + /** * Creates recruitCRM reducer with the specified initial state. * @param {Object} state Optional. If not given, the default one is @@ -70,6 +96,8 @@ function create(state = {}) { [actions.recruit.getJobsDone]: onDone, [actions.recruit.getJobInit]: onJobInit, [actions.recruit.getJobDone]: onJobDone, + [actions.recruit.applyForJobInit]: onApplyForJobInit, + [actions.recruit.applyForJobDone]: onApplyForJobDone, }, state); } diff --git a/src/shared/services/recruitCRM.js b/src/shared/services/recruitCRM.js index 0c124ef6ee..e20cccb5f4 100644 --- a/src/shared/services/recruitCRM.js +++ b/src/shared/services/recruitCRM.js @@ -1,6 +1,7 @@ import fetch from 'isomorphic-fetch'; import { logger } from 'topcoder-react-lib'; import qs from 'qs'; +import _ from 'lodash'; const PROXY_ENDPOINT = '/api/recruit'; @@ -9,7 +10,7 @@ export default class Service { /** * Get jobs by query - * @param {*} query The request query + * @param {object} query The request query */ async getJobs(query) { const res = await fetch(`${this.baseUrl}/jobs/search?${qs.stringify(query)}`); @@ -35,7 +36,7 @@ export default class Service { /** * Get all jobs - * @param {*} query The request query + * @param {object} query The request query */ async getAllJobs(query) { const res = await fetch(`${this.baseUrl}/jobs?${qs.stringify(query)}`); @@ -45,4 +46,25 @@ export default class Service { } return res.json(); } + + /** + * applyForJob for candidate + * @param {string} id The job id to apply to + * @param {object} payload The apply payload + */ + async applyForJob(id, payload) { + const { resume } = payload; + const data = new FormData(); + data.append('resume', resume); + data.append('form', JSON.stringify(_.omit(payload, 'resume'))); + const res = await fetch(`${this.baseUrl}/jobs/${id}/apply`, { + method: 'POST', + body: data, + }); + if (!res.ok) { + const error = new Error('Failed to apply for job'); + logger.error(error, res); + } + return res.json(); + } } From 764132305681e1a19ffbc55de1fe2ccace414b3e Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Fri, 25 Sep 2020 16:07:39 +0300 Subject: [PATCH 05/25] Country dropdown --- src/shared/actions/recruitCRM.js | 3 ++- src/shared/components/GUIKit/Dropdown/style.scss | 1 + src/shared/components/Gigs/GigApply/index.jsx | 7 ++++--- src/shared/components/Gigs/GigApply/style.scss | 5 +++++ src/shared/containers/Gigs/RecruitCRMJobApply.jsx | 9 ++++++++- 5 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/shared/actions/recruitCRM.js b/src/shared/actions/recruitCRM.js index 9e153d787f..13259afccd 100644 --- a/src/shared/actions/recruitCRM.js +++ b/src/shared/actions/recruitCRM.js @@ -1,5 +1,6 @@ import { redux } from 'topcoder-react-utils'; import Service from 'services/recruitCRM'; +import _ from 'lodash'; /** * Jobs page fetch init @@ -58,7 +59,7 @@ function normalizeRecruitPayload(payload) { email: payload.email, contact_number: payload.phone, city: payload.city, - locality: payload.country, + locality: _.find(payload.country, { selected: true }).label, available_from: payload.availFrom, salary_expectation: payload.payExpectation, skill: payload.skills.filter(s => s.selected).map(s => s.label).join(','), diff --git a/src/shared/components/GUIKit/Dropdown/style.scss b/src/shared/components/GUIKit/Dropdown/style.scss index 8f58c28383..79a2fc4451 100644 --- a/src/shared/components/GUIKit/Dropdown/style.scss +++ b/src/shared/components/GUIKit/Dropdown/style.scss @@ -27,6 +27,7 @@ padding-top: 12px; &.haveValue .label, + &.haveError .label, &.isFocused .label { display: flex; } diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx index 2768e394c1..733bba344f 100644 --- a/src/shared/components/Gigs/GigApply/index.jsx +++ b/src/shared/components/Gigs/GigApply/index.jsx @@ -14,6 +14,7 @@ import Checkbox from 'components/GUIKit/Checkbox'; import { getCustomField } from 'utils/gigs'; import Modal from 'components/Contentful/Modal'; import FilestackFilePicker from 'components/GUIKit/FilePicker'; +import Dropdown from 'components/GUIKit/Dropdown'; import './style.scss'; import bigCheckmark from 'assets/images/big-checkmark.png'; import SadFace from 'assets/images/sad-face-icon.svg'; @@ -72,7 +73,7 @@ export default function GigApply(props) { } { !application ? ( -
+

PERSONAL INFORMATION

Welcome to Topcoder Gigs! We’d like to get to know you.

@@ -121,12 +122,12 @@ export default function GigApply(props) { value={formData.city} required /> - onFormInputChange('country', val)} errorMsg={formErrors.country} - value={formData.country} + options={formData.country} required />
diff --git a/src/shared/components/Gigs/GigApply/style.scss b/src/shared/components/Gigs/GigApply/style.scss index b109208cf7..9f6942a5b6 100644 --- a/src/shared/components/Gigs/GigApply/style.scss +++ b/src/shared/components/Gigs/GigApply/style.scss @@ -245,4 +245,9 @@ } /* stylelint-enable */ } + + .form-wrap.applying .form-section { + opacity: 0.6; + pointer-events: none !important; + } } diff --git a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx index b492fa680a..1f40c69819 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx @@ -11,6 +11,9 @@ import { connect } from 'react-redux'; import { isValidEmail } from 'utils/tc'; import techSkills from './techSkills'; +const countries = require('i18n-iso-countries'); +countries.registerLocale(require('i18n-iso-countries/langs/en.json')); + class RecruitCRMJobApplyContainer extends React.Component { constructor(props) { super(props); @@ -26,6 +29,7 @@ class RecruitCRMJobApplyContainer extends React.Component { { label: '10 hours', checked: false }, { label: '20 hours', checked: false }, { label: '30 hours', checked: false }, { label: '40 hours', checked: false }, ], agreedTerms: false, + country: _.map(countries.getNames('en'), val => ({ label: val, selected: false })), // eslint-disable-next-line react/destructuring-assignment }, }; @@ -69,7 +73,7 @@ class RecruitCRMJobApplyContainer extends React.Component { const { formData, formErrors } = state; // Form validation happens here const requiredTextFields = [ - 'fname', 'lname', 'city', 'country', 'reffereal', 'phone', 'email', + 'fname', 'lname', 'city', 'reffereal', 'phone', 'email', ]; // check required text fields for value _.each(requiredTextFields, (key) => { @@ -77,6 +81,9 @@ class RecruitCRMJobApplyContainer extends React.Component { else if (formData[key] && _.trim(formData[key]).length < 2) formErrors[key] = 'Must be at least 2 characters'; else delete formErrors[key]; }); + // check for selected country + if (!_.find(formData.country, { selected: true })) formErrors.country = 'Please, select your country'; + else delete formErrors.country; // check payExpectation to be a number if (formData.payExpectation && _.trim(formData.payExpectation)) { if (!_.isInteger(_.toNumber(formData.payExpectation))) formErrors.payExpectation = 'Must be integer value in $'; From 547898bb5825b94a3c1d7f6543ca62bde468648f Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 28 Sep 2020 16:54:03 -0300 Subject: [PATCH 06/25] SubmissionManagement - Get challenge status from utils/tc --- .../components/SubmissionManagement/Submission/index.jsx | 4 ++-- src/shared/utils/tc.js | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/shared/components/SubmissionManagement/Submission/index.jsx b/src/shared/components/SubmissionManagement/Submission/index.jsx index 3c82660f7f..dd4e337b21 100644 --- a/src/shared/components/SubmissionManagement/Submission/index.jsx +++ b/src/shared/components/SubmissionManagement/Submission/index.jsx @@ -15,7 +15,7 @@ import _ from 'lodash'; import moment from 'moment'; import React from 'react'; import { config } from 'topcoder-react-utils'; -import { COMPETITION_TRACKS } from 'utils/tc'; +import { COMPETITION_TRACKS, CHALLENGE_STATUS } from 'utils/tc'; import PT from 'prop-types'; @@ -85,7 +85,7 @@ export default function Submission(props) { onClick={() => onDownload(submissionObject.id)} > */ } - {status !== 'COMPLETED' + {status !== CHALLENGE_STATUS.COMPLETED && ( */ } {status !== CHALLENGE_STATUS.COMPLETED + && track !== COMPETITION_TRACKS.DESIGN && (
) : (
+ back-arrow-icon GIG DETAILS

{job.name}

- GIG DETAILS
{ application ? ( @@ -72,8 +74,16 @@ export default function GigApply(props) { ) : null } { - !application ? ( -
+ applying ? ( +
+ +

Processing your application…

+
+ ) : null + } + { + !application && !applying ? ( +

PERSONAL INFORMATION

Welcome to Topcoder Gigs! We’d like to get to know you.

diff --git a/src/shared/components/Gigs/GigApply/style.scss b/src/shared/components/Gigs/GigApply/style.scss index 9f6942a5b6..783b7424c9 100644 --- a/src/shared/components/Gigs/GigApply/style.scss +++ b/src/shared/components/Gigs/GigApply/style.scss @@ -1,9 +1,22 @@ @import '~styles/mixins'; @import "~components/Contentful/default"; +.loading-wrap { + margin-top: 35px; + + .loading-text { + font-family: Roboto, sans-serif; + font-size: 24px; + line-height: 26px; + color: #2a2a2a; + text-align: center; + margin-top: 26px; + } +} + .container { max-width: $screen-lg; - min-height: 50vh; + min-height: 80vh; margin: auto; color: #2a2a2a; @@ -46,8 +59,8 @@ h2 { color: #26b3c5; text-align: center; - margin-top: 47px; - margin-bottom: 40px; + margin-top: 6px; + margin-bottom: 83px; @include xs-to-md { text-align: left; @@ -77,6 +90,8 @@ h2 { color: #2a2a2a; + margin-top: 31px; + margin-bottom: 20px; } p { @@ -89,6 +104,10 @@ line-height: 36px; } } + + .cta-buttons { + margin-top: 30px; + } } .form-wrap { @@ -103,6 +122,10 @@ p { font-size: 14px; line-height: 22px; + + a { + font-size: 14px; + } } .form-section { @@ -112,7 +135,7 @@ display: grid; gap: 20px; grid-template-columns: 1fr 1fr; - margin-bottom: 20px; + margin-bottom: 8px; @include xs-to-md { display: flex; @@ -212,7 +235,7 @@ } } } - +/* stylelint-disable */ .cta-buttons { display: flex; justify-content: center; @@ -222,7 +245,6 @@ flex-direction: column; } - /* stylelint-disable */ a { background-color: #fff; border: 1px solid #137d60; @@ -243,11 +265,6 @@ text-align: center; } } - /* stylelint-enable */ - } - - .form-wrap.applying .form-section { - opacity: 0.6; - pointer-events: none !important; } + /* stylelint-enable */ } diff --git a/src/shared/components/Gigs/GigDetails/index.jsx b/src/shared/components/Gigs/GigDetails/index.jsx index a0be8d26d4..938c1003f0 100644 --- a/src/shared/components/Gigs/GigDetails/index.jsx +++ b/src/shared/components/Gigs/GigDetails/index.jsx @@ -35,7 +35,7 @@ const ReactHtmlParserOptions = { }; export default function GigDetails(props) { - const { job } = props; + const { job, application } = props; let shareUrl; if (isomorphy.isClientSide()) { shareUrl = encodeURIComponent(window.location.href); @@ -110,7 +110,11 @@ export default function GigDetails(props) {
- APPLY TO THIS JOB + { + !application || !application.success ? ( + APPLY TO THIS JOB + ) : null + } VIEW OTHER JOBS
@@ -159,6 +163,11 @@ export default function GigDetails(props) { ); } +GigDetails.defaultProps = { + application: null, +}; + GigDetails.propTypes = { job: PT.shape().isRequired, + application: PT.shape(), }; diff --git a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx index 297ff39dd6..9eaa7fe01f 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx @@ -46,7 +46,6 @@ class RecruitCRMJobApplyContainer extends React.Component { this.setState({ formData: _.merge(formData, user), }); - this.validateForm(); } onFormInputChange(key, value) { @@ -65,7 +64,12 @@ class RecruitCRMJobApplyContainer extends React.Component { onApplyClick() { const { applyForJob, job } = this.props; const { formData } = this.state; - applyForJob(job, formData); + this.validateForm(); + this.setState((state) => { + if (_.isEmpty(state.formErrors)) { + applyForJob(job, formData); + } + }); } validateForm() { diff --git a/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx index 7479afa0fe..7be3ddf9f6 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx @@ -30,18 +30,21 @@ class RecruitCRMJobDetailsContainer extends React.Component { loading, job, isApply, + application, } = this.props; if (loading) { return ; } - return isApply ? : ; + return isApply + ? : ; } } RecruitCRMJobDetailsContainer.defaultProps = { job: {}, + application: null, }; RecruitCRMJobDetailsContainer.propTypes = { @@ -50,6 +53,7 @@ RecruitCRMJobDetailsContainer.propTypes = { job: PT.shape(), id: PT.string.isRequired, isApply: PT.bool.isRequired, + application: PT.shape(), }; function mapStateToProps(state, ownProps) { @@ -57,6 +61,7 @@ function mapStateToProps(state, ownProps) { return { job: data ? data.job : {}, loading: data ? data.loading : true, + application: data ? data.application : null, }; } From ebb4c29ecc932703f25b33da09ea2fb6a15bc5bf Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 30 Sep 2020 11:42:13 +0300 Subject: [PATCH 16/25] Better errors for gig details page --- src/shared/components/Gigs/GigDetails/index.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/shared/components/Gigs/GigDetails/index.jsx b/src/shared/components/Gigs/GigDetails/index.jsx index 938c1003f0..2471687f42 100644 --- a/src/shared/components/Gigs/GigDetails/index.jsx +++ b/src/shared/components/Gigs/GigDetails/index.jsx @@ -22,6 +22,7 @@ import iconSkills from 'assets/images/icon-skills-blue.png'; import iconLabel1 from 'assets/images/l1.png'; import iconLabel2 from 'assets/images/l2.png'; import iconLabel3 from 'assets/images/l3.png'; +import SadFace from 'assets/images/sad-face-icon.svg'; // Cleanup HTML from style tags // so it won't affect other parts of the UI @@ -48,7 +49,8 @@ export default function GigDetails(props) { { job.error || job.enable_job_application_form !== 1 ? (
-

Gig does not exist.

+ { job.error ? : null } +

{ job.error ? 'Gig does not exist' : 'This Gig has been Fulfilled'}

VIEW OTHER GIGS
From 6f97511390f490ad915e10e51daca4560b888a69 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 30 Sep 2020 13:33:16 +0300 Subject: [PATCH 17/25] Date picker fixes --- src/shared/components/GUIKit/Datepicker/index.jsx | 2 +- src/shared/components/Gigs/GigApply/index.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/shared/components/GUIKit/Datepicker/index.jsx b/src/shared/components/GUIKit/Datepicker/index.jsx index c67f50de7c..5cf376d7ef 100644 --- a/src/shared/components/GUIKit/Datepicker/index.jsx +++ b/src/shared/components/GUIKit/Datepicker/index.jsx @@ -41,7 +41,7 @@ function Datepicker({ date={date} onDateChange={(changedDate) => { setDate(changedDate); - onChange(changedDate.toDate()); + onChange(changedDate ? changedDate.toDate() : null); }} focused={focused} onFocusChange={({ focused: changedFocused }) => setFocused(changedFocused) diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx index d3c295d26e..320327e6a6 100644 --- a/src/shared/components/Gigs/GigApply/index.jsx +++ b/src/shared/components/Gigs/GigApply/index.jsx @@ -176,7 +176,7 @@ export default function GigApply(props) { onFormInputChange('availFrom', val.toISOString())} + onChange={val => onFormInputChange('availFrom', val ? val.toISOString() : null)} errorMsg={formErrors.availFrom} value={formData.availFrom} /> From 96385a7c1f3a141c4e9d493a32f91623682134ef Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 30 Sep 2020 22:57:04 +0300 Subject: [PATCH 18/25] Implements #5023 --- .../Preferences/Email/__snapshots__/index.jsx.snap | 8 ++++++++ .../components/Settings/Preferences/Email/index.jsx | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/__tests__/shared/components/Settings/Preferences/Email/__snapshots__/index.jsx.snap b/__tests__/shared/components/Settings/Preferences/Email/__snapshots__/index.jsx.snap index cde7b4e722..ab117b50c9 100644 --- a/__tests__/shared/components/Settings/Preferences/Email/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/Settings/Preferences/Email/__snapshots__/index.jsx.snap @@ -65,6 +65,14 @@ exports[`renders email preferences setting page correctly 1`] = ` secondaryText="For all the latest updates surrounding the Topcoder Open you should definitely be subscribing to this one. Expect an update in your mailbox every Tuesday!" value="TCO Tuesdays" /> +
`; diff --git a/src/shared/components/Settings/Preferences/Email/index.jsx b/src/shared/components/Settings/Preferences/Email/index.jsx index cea54fb30c..b8666c7a80 100644 --- a/src/shared/components/Settings/Preferences/Email/index.jsx +++ b/src/shared/components/Settings/Preferences/Email/index.jsx @@ -55,6 +55,11 @@ const newsletters = [ name: 'TCO Newsletter', desc: 'For all the latest updates surrounding the Topcoder Open you should definitely be subscribing to this one. Expect an update in your mailbox every Tuesday!', }, + { + id: 'RDM', + name: 'Rapid Development Match (RDM) Reminders', + desc: 'Receive notifications of our brand new RDMs! These rated, development matches will be a fun new way to engage with us!', + }, ]; export default class EmailPreferences extends React.Component { From ea9fd9dc9ff75361e7e41cd5cb93839fdc720228 Mon Sep 17 00:00:00 2001 From: Luiz Ricardo Rodrigues Date: Wed, 30 Sep 2020 17:41:51 -0300 Subject: [PATCH 19/25] ci: deploy rdm-email-switch to QA env #5023 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 539c227375..58deec9aea 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -245,7 +245,7 @@ workflows: filters: branches: only: - - milestone-20200917 + - rdm-email-switch - develop # This is beta env for production soft releases - "build-prod-beta": From 0633ef39f02eb704a2b5b9a3b9ee6804b3670564 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Thu, 1 Oct 2020 00:30:56 +0300 Subject: [PATCH 20/25] Apply page fixes --- src/assets/images/back-arrow-gig-apply.png | Bin 176 -> 0 bytes src/assets/images/back-arrow-gig-apply.svg | 17 +++++++++++++++++ src/shared/components/Gigs/GigApply/index.jsx | 4 ++-- src/shared/components/Gigs/GigApply/style.scss | 11 +++++++++-- .../containers/Gigs/RecruitCRMJobApply.jsx | 2 -- 5 files changed, 28 insertions(+), 6 deletions(-) delete mode 100644 src/assets/images/back-arrow-gig-apply.png create mode 100644 src/assets/images/back-arrow-gig-apply.svg diff --git a/src/assets/images/back-arrow-gig-apply.png b/src/assets/images/back-arrow-gig-apply.png deleted file mode 100644 index 7f623eaffe23bb0851c75c6a927d8ce26e4d76aa..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 176 zcmeAS@N?(olHy`uVBq!ia0vp^96-#&!VDzWZg{v3NXZ5Ggt!7}U0q!@H8m9#6>Tuk z)YQ}hga7~k&%Nd20aU?J666=mASfa#apvN+JxzT6KrzN7Z+92Yk2jaT0djOaT^vI= zt|uoXBm@Dmfp%g-!Z{Bnqgx^ZJZu-s*xA^QaV${b64v0o!p87PpM%RPYDzg!AA_f> KpUXO@geCwGSS?-v diff --git a/src/assets/images/back-arrow-gig-apply.svg b/src/assets/images/back-arrow-gig-apply.svg new file mode 100644 index 0000000000..ab3b9f0bbb --- /dev/null +++ b/src/assets/images/back-arrow-gig-apply.svg @@ -0,0 +1,17 @@ + + + + +BD4BD0F4-C75C-4897-A58C-B092051E2535 +Created with sketchtool. + + + + + + + + diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx index 320327e6a6..29adef4521 100644 --- a/src/shared/components/Gigs/GigApply/index.jsx +++ b/src/shared/components/Gigs/GigApply/index.jsx @@ -19,7 +19,7 @@ import LoadingIndicator from 'components/LoadingIndicator'; import './style.scss'; import bigCheckmark from 'assets/images/big-checkmark.png'; import SadFace from 'assets/images/sad-face-icon.svg'; -import backArrowGig from 'assets/images/back-arrow-gig-apply.png'; +import BackArrowGig from 'assets/images/back-arrow-gig-apply.svg'; export default function GigApply(props) { const { @@ -38,8 +38,8 @@ export default function GigApply(props) {
) : (
- back-arrow-icon GIG DETAILS

{job.name}

+ GIG DETAILS
{ application ? ( diff --git a/src/shared/components/Gigs/GigApply/style.scss b/src/shared/components/Gigs/GigApply/style.scss index 783b7424c9..c5b059206c 100644 --- a/src/shared/components/Gigs/GigApply/style.scss +++ b/src/shared/components/Gigs/GigApply/style.scss @@ -59,8 +59,8 @@ h2 { color: #26b3c5; text-align: center; - margin-top: 6px; - margin-bottom: 83px; + margin-top: 47px; + margin-bottom: 31px; @include xs-to-md { text-align: left; @@ -75,6 +75,13 @@ text-decoration: none; line-height: 40px; margin-bottom: 12px; + display: flex; + align-items: center; + + svg { + width: 8px; + margin-right: 6px; + } } .separator { diff --git a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx index 9eaa7fe01f..0a1b7aa7f5 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx @@ -57,8 +57,6 @@ class RecruitCRMJobApplyContainer extends React.Component { [key]: value, }, })); - // run form validation - this.validateForm(); } onApplyClick() { From ef6423637761afe1fcd1c31d5ea1df0e7ba615e5 Mon Sep 17 00:00:00 2001 From: Luiz Ricardo Rodrigues Date: Thu, 1 Oct 2020 00:13:04 -0300 Subject: [PATCH 21/25] ci: Deploy tcx-202010 to Stag env Issues: https://github.com/topcoder-platform/community-app/issues/4926 https://github.com/topcoder-platform/community-app/issues/4948 https://github.com/topcoder-platform/community-app/issues/5004 --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 89dec25ea4..6ea3bff7c0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -245,7 +245,6 @@ workflows: filters: branches: only: - - tcx-202010 - develop # This is beta env for production soft releases - "build-prod-beta": @@ -262,6 +261,7 @@ workflows: branches: only: - develop + - tcx-202010 # Production builds are exectuted # when PR is merged to the master # Don't change anything in this configuration From 8836b4a1871e24b904fd19c9f5e74a256f134089 Mon Sep 17 00:00:00 2001 From: Luiz Ricardo Rodrigues Date: Thu, 1 Oct 2020 03:22:38 -0300 Subject: [PATCH 22/25] fix: for issue #4948 #4948 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 81d414873a..5649814629 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "tc-accounts": "git+https://github.com/appirio-tech/accounts-app.git#dev", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", "tc-ui": "^1.0.12", - "topcoder-react-lib": "1000.22.12", + "topcoder-react-lib": "1000.22.13", "topcoder-react-ui-kit": "2.0.1", "topcoder-react-utils": "0.7.8", "turndown": "^4.0.2", From 5151e2dbc282b5c01fe832f4e14d1abe6e8f123e Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Thu, 1 Oct 2020 10:15:08 +0300 Subject: [PATCH 23/25] Fix validation for #4891 --- .../containers/Gigs/RecruitCRMJobApply.jsx | 66 ++++++++++++------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx index 0a1b7aa7f5..cb1ee664c7 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx @@ -57,6 +57,7 @@ class RecruitCRMJobApplyContainer extends React.Component { [key]: value, }, })); + this.validateForm(key); } onApplyClick() { @@ -70,7 +71,7 @@ class RecruitCRMJobApplyContainer extends React.Component { }); } - validateForm() { + validateForm(prop) { this.setState((state) => { const { formData, formErrors } = state; // Form validation happens here @@ -80,6 +81,9 @@ class RecruitCRMJobApplyContainer extends React.Component { // check required text fields for value // check min/max lengths _.each(requiredTextFields, (key) => { + // validate only modified prop if set + // and do not touch the others + if (prop && prop !== key) return; if (!formData[key] || !_.trim(formData[key])) formErrors[key] = 'Required field'; else if (formData[key] && _.trim(formData[key]).length < 2) formErrors[key] = 'Must be at least 2 characters'; else if (formData[key] && _.trim(formData[key]).length > 2) { @@ -101,36 +105,48 @@ class RecruitCRMJobApplyContainer extends React.Component { } else delete formErrors[key]; }); // check for selected country - if (!_.find(formData.country, { selected: true })) formErrors.country = 'Please, select your country'; - else delete formErrors.country; + if (!prop || prop === 'country') { + if (!_.find(formData.country, { selected: true })) formErrors.country = 'Please, select your country'; + else delete formErrors.country; + } // check payExpectation to be a number - if (formData.payExpectation && _.trim(formData.payExpectation)) { - if (!_.isInteger(_.toNumber(formData.payExpectation))) formErrors.payExpectation = 'Must be integer value in $'; - else delete formErrors.payExpectation; - } else delete formErrors.payExpectation; + if (!prop || prop === 'payExpectation') { + if (formData.payExpectation && _.trim(formData.payExpectation)) { + if (!_.isInteger(_.toNumber(formData.payExpectation))) formErrors.payExpectation = 'Must be integer value in $'; + else delete formErrors.payExpectation; + } else delete formErrors.payExpectation; + } // check for valid email - if (formData.email && _.trim(formData.email)) { - if (!(isValidEmail(formData.email))) formErrors.email = 'Invalid email'; - else delete formErrors.email; + if (!prop || prop === 'email') { + if (formData.email && _.trim(formData.email)) { + if (!(isValidEmail(formData.email))) formErrors.email = 'Invalid email'; + else delete formErrors.email; + } } // require atleast 1 skill - if (!_.find(formData.skills, { selected: true })) formErrors.skills = 'Please, add technical skills'; - else delete formErrors.skills; + if (!prop || prop === 'skills') { + if (!_.find(formData.skills, { selected: true })) formErrors.skills = 'Please, add technical skills'; + else delete formErrors.skills; + } // have accepted terms - if (!formData.agreedTerms) formErrors.agreedTerms = 'Please, accept our terms'; - else delete formErrors.agreedTerms; + if (!prop || prop === 'agreedTerms') { + if (!formData.agreedTerms) formErrors.agreedTerms = 'Please, accept our terms'; + else delete formErrors.agreedTerms; + } // has CV file ready for upload - if (!formData.fileCV) formErrors.fileCV = 'Please, pick your CV file for uploading'; - else { - const sizeInMB = (formData.fileCV.size / (1024 * 1024)).toFixed(2); - if (sizeInMB > 8) { - formErrors.fileCV = 'Max file size is limited to 8 MB'; - delete formData.fileCV; - } else if (_.endsWith(formData.fileCV.name, '.pdf') || _.endsWith(formData.fileCV.name, '.docx')) { - delete formErrors.fileCV; - } else { - formErrors.fileCV = 'Only .pdf and .docx files are allowed'; - delete formErrors.fileCV; + if (!prop || prop === 'fileCV') { + if (!formData.fileCV) formErrors.fileCV = 'Please, pick your CV file for uploading'; + else { + const sizeInMB = (formData.fileCV.size / (1024 * 1024)).toFixed(2); + if (sizeInMB > 8) { + formErrors.fileCV = 'Max file size is limited to 8 MB'; + delete formData.fileCV; + } else if (_.endsWith(formData.fileCV.name, '.pdf') || _.endsWith(formData.fileCV.name, '.docx')) { + delete formErrors.fileCV; + } else { + formErrors.fileCV = 'Only .pdf and .docx files are allowed'; + delete formErrors.fileCV; + } } } // updated state From 4adf0fc687f856b0b9e58b6b9362fb1ae1b70818 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Thu, 1 Oct 2020 11:33:26 +0300 Subject: [PATCH 24/25] Fixed file validation --- src/shared/containers/Gigs/RecruitCRMJobApply.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx index cb1ee664c7..67a67e792d 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx @@ -145,7 +145,6 @@ class RecruitCRMJobApplyContainer extends React.Component { delete formErrors.fileCV; } else { formErrors.fileCV = 'Only .pdf and .docx files are allowed'; - delete formErrors.fileCV; } } } From 80cb5b3a306b50f09f5b0ede943fa205f46db1d2 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Thu, 1 Oct 2020 19:06:43 +0530 Subject: [PATCH 25/25] release: v1.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f89cbaede7..adf6e8f71c 100644 --- a/package.json +++ b/package.json @@ -140,7 +140,7 @@ "tc-accounts": "git+https://github.com/appirio-tech/accounts-app.git#dev", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", "tc-ui": "^1.0.12", - "topcoder-react-lib": "1000.22.13", + "topcoder-react-lib": "1.0.7", "topcoder-react-ui-kit": "2.0.1", "topcoder-react-utils": "0.7.8", "turndown": "^4.0.2",