diff --git a/.circleci/config.yml b/.circleci/config.yml
index 6f3b9fc80c..e3e323f801 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -245,7 +245,7 @@ workflows:
filters:
branches:
only:
- - develop
+ - gig-application
# This is beta env for production soft releases
- "build-prod-beta":
context : org-global
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`] = `
-
+
+
+ {errorMsg ? (
{errorMsg}) : null}
);
}
@@ -48,12 +52,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..28da3d2e24 100644
--- a/src/shared/components/GUIKit/Checkbox/style.scss
+++ b/src/shared/components/GUIKit/Checkbox/style.scss
@@ -8,20 +8,16 @@
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;
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;
- }
- }
}
}
@@ -46,8 +42,8 @@
border-radius: 4px;
.after {
- margin-left: -8px;
- margin-top: -9px;
+ margin-left: -9px;
+ margin-top: -7px;
}
}
}
@@ -63,8 +59,8 @@
border-radius: 3px;
.after {
- margin-left: -6px;
- margin-top: -7px;
+ margin-left: -7px;
+ margin-top: -6px;
}
}
}
@@ -80,8 +76,8 @@
border-radius: 2px;
.after {
- margin-left: -5px;
- margin-top: -5px;
+ margin-left: -6px;
+ margin-top: -4px;
}
}
}
@@ -107,3 +103,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..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;
}
@@ -147,6 +148,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 }) => (
+
+ )}
+
+ {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/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..7089935faf 100644
--- a/src/shared/components/GUIKit/TextInput/style.scss
+++ b/src/shared/components/GUIKit/TextInput/style.scss
@@ -41,12 +41,13 @@
}
input:not([type='checkbox']).haveValue + label,
+ input:not([type='checkbox']).haveError + label,
input:not([type='checkbox']):focus + label {
display: flex;
}
input:not([type='checkbox']):focus + label {
- color: $gui-kit-level-2;
+ color: $gui-kit-active-label;
}
input:not([type='checkbox']).haveError + label,
diff --git a/src/shared/components/Gigs/GigApply/index.jsx b/src/shared/components/Gigs/GigApply/index.jsx
new file mode 100644
index 0000000000..29adef4521
--- /dev/null
+++ b/src/shared/components/Gigs/GigApply/index.jsx
@@ -0,0 +1,310 @@
+/**
+ * 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 Dropdown from 'components/GUIKit/Dropdown';
+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.svg';
+
+export default function GigApply(props) {
+ const {
+ job, onFormInputChange, formData, formErrors, onApplyClick, applying, application,
+ } = props;
+
+ return (
+
+ {
+ job.error || job.enable_job_application_form !== 1 ? (
+
+
Gig does not exist.
+
+ VIEW OTHER GIGS
+
+
+ ) : (
+
+
{job.name}
+
GIG DETAILS
+
+ {
+ application ? (
+
+ { application.error ?
:

}
+
{application.error ? 'OOPS!' : 'APPLICATION SUBMITTED'}
+ {
+ 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!
+ )
+ }
+
+
+ ) : null
+ }
+ {
+ applying ? (
+
+
+
Processing your application…
+
+ ) : null
+ }
+ {
+ !application && !applying ? (
+
+
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}
+ options={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 ? val.toISOString() : null)}
+ 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"
+ />
+
+ 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
+
+
+
+ ) : null
+ }
+
+ )
+ }
+
+ );
+}
+
+GigApply.defaultProps = {
+ formErrors: {},
+ applying: false,
+ application: null,
+};
+
+GigApply.propTypes = {
+ job: PT.shape().isRequired,
+ formErrors: PT.shape(),
+ 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
new file mode 100644
index 0000000000..c5b059206c
--- /dev/null
+++ b/src/shared/components/Gigs/GigApply/style.scss
@@ -0,0 +1,277 @@
+@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: 80vh;
+ 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: 31px;
+
+ @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;
+ display: flex;
+ align-items: center;
+
+ svg {
+ width: 8px;
+ margin-right: 6px;
+ }
+ }
+
+ .separator {
+ border-bottom: 1px solid #e9e9e9;
+ }
+
+ .apply-state {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-bottom: 162px;
+ margin-top: 50px;
+
+ h2 {
+ color: #2a2a2a;
+ margin-top: 31px;
+ margin-bottom: 20px;
+ }
+
+ p {
+ font-size: 24px;
+ line-height: 36px;
+ text-align: center;
+
+ a {
+ font-size: 24px;
+ line-height: 36px;
+ }
+ }
+
+ .cta-buttons {
+ margin-top: 30px;
+ }
+ }
+
+ .form-wrap {
+ max-width: 880px;
+ margin: auto;
+
+ h4 {
+ margin-top: 35px;
+ margin-bottom: 5px;
+ }
+
+ p {
+ font-size: 14px;
+ line-height: 22px;
+
+ a {
+ font-size: 14px;
+ }
+ }
+
+ .form-section {
+ margin: 13px 0 50px;
+
+ .form-row {
+ display: grid;
+ gap: 20px;
+ grid-template-columns: 1fr 1fr;
+ margin-bottom: 8px;
+
+ @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;
+ }
+ }
+ }
+/* stylelint-disable */
+ .cta-buttons {
+ display: flex;
+ justify-content: center;
+ margin-top: 47px;
+
+ @include xs-to-sm {
+ flex-direction: column;
+ }
+
+ 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 92%
rename from src/shared/components/Gigs/GigDetails.jsx
rename to src/shared/components/Gigs/GigDetails/index.jsx
index df0b06f343..2471687f42 100644
--- a/src/shared/components/Gigs/GigDetails.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
@@ -35,7 +36,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);
@@ -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
@@ -110,7 +112,11 @@ export default function GigDetails(props) {
-
APPLY TO THIS JOB
+ {
+ !application || !application.success ? (
+
APPLY TO THIS JOB
+ ) : null
+ }
VIEW OTHER JOBS
@@ -159,6 +165,11 @@ export default function GigDetails(props) {
);
}
+GigDetails.defaultProps = {
+ application: null,
+};
+
GigDetails.propTypes = {
job: PT.shape().isRequired,
+ application: PT.shape(),
};
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..67a67e792d
--- /dev/null
+++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx
@@ -0,0 +1,222 @@
+/**
+ * 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';
+
+const countries = require('i18n-iso-countries');
+countries.registerLocale(require('i18n-iso-countries/langs/en.json'));
+
+class RecruitCRMJobApplyContainer extends React.Component {
+ constructor(props) {
+ super(props);
+ // initial state
+ this.state = {
+ formErrors: {},
+ 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 }],
+ timeAvailability: [
+ { 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
+ },
+ };
+
+ // binds
+ this.onFormInputChange = this.onFormInputChange.bind(this);
+ this.onApplyClick = this.onApplyClick.bind(this);
+ this.validateForm = this.validateForm.bind(this);
+ }
+
+ componentDidMount() {
+ const { formData } = this.state;
+ const { user } = this.props;
+ this.setState({
+ formData: _.merge(formData, user),
+ });
+ }
+
+ onFormInputChange(key, value) {
+ // update the state
+ this.setState(state => ({
+ ...state,
+ formData: {
+ ...state.formData,
+ [key]: value,
+ },
+ }));
+ this.validateForm(key);
+ }
+
+ onApplyClick() {
+ const { applyForJob, job } = this.props;
+ const { formData } = this.state;
+ this.validateForm();
+ this.setState((state) => {
+ if (_.isEmpty(state.formErrors)) {
+ applyForJob(job, formData);
+ }
+ });
+ }
+
+ validateForm(prop) {
+ this.setState((state) => {
+ const { formData, formErrors } = state;
+ // Form validation happens here
+ const requiredTextFields = [
+ 'fname', 'lname', 'city', 'reffereal', 'phone', 'email',
+ ];
+ // 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) {
+ switch (key) {
+ case 'reffereal':
+ if (_.trim(formData[key]).length > 2000) formErrors[key] = 'Must be max 2000 characters';
+ else delete formErrors[key];
+ break;
+ case 'city':
+ case 'phone':
+ if (_.trim(formData[key]).length > 50) formErrors[key] = 'Must be max 50 characters';
+ else delete formErrors[key];
+ break;
+ default:
+ if (_.trim(formData[key]).length > 40) formErrors[key] = 'Must be max 40 characters';
+ else delete formErrors[key];
+ break;
+ }
+ } else delete formErrors[key];
+ });
+ // check for selected 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 (!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 (!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 (!prop || prop === 'skills') {
+ if (!_.find(formData.skills, { selected: true })) formErrors.skills = 'Please, add technical skills';
+ else delete formErrors.skills;
+ }
+ // have accepted terms
+ if (!prop || prop === 'agreedTerms') {
+ if (!formData.agreedTerms) formErrors.agreedTerms = 'Please, accept our terms';
+ else delete formErrors.agreedTerms;
+ }
+ // has CV file ready for upload
+ 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';
+ }
+ }
+ }
+ // updated state
+ return {
+ ...state,
+ formErrors,
+ };
+ });
+ }
+
+ render() {
+ const { formErrors, formData } = this.state;
+ return (
+