diff --git a/.circleci/config.yml b/.circleci/config.yml
index bdd7e19122..4c79f56969 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -290,7 +290,7 @@ workflows:
filters:
branches:
only:
- - free
+ - gig-ab-v2
# This is beta env for production soft releases
- "build-prod-beta":
context : org-global
diff --git a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx
index cba026118b..d74b876ec7 100644
--- a/src/shared/containers/Gigs/RecruitCRMJobApply.jsx
+++ b/src/shared/containers/Gigs/RecruitCRMJobApply.jsx
@@ -14,6 +14,7 @@ import { withOptimizely } from '@optimizely/react-sdk';
import techSkills from './techSkills';
+const cookies = require('browser-cookies');
const countries = require('i18n-iso-countries');
countries.registerLocale(require('i18n-iso-countries/langs/en.json'));
@@ -129,6 +130,22 @@ class RecruitCRMJobApplyContainer extends React.Component {
if (_.isEmpty(state.formErrors)) {
applyForJob(job, formData);
optimizely.track('Submit Application Form');
+ const isFeatured = _.find(job.custom_fields, ['field_name', 'Featured']);
+ const jobTags = _.find(job.custom_fields, ['field_name', 'Job Tag']);
+ let hotListCookie = cookies.get('_tc.hcl');
+ if (isFeatured && isFeatured.value) {
+ optimizely.track('Submit to Featured Gigs');
+ }
+ if (jobTags && jobTags.value) {
+ optimizely.track('Submit to Tagged Gigs');
+ }
+ if (hotListCookie) {
+ hotListCookie = JSON.parse(hotListCookie);
+ if (hotListCookie.slug === job.slug) {
+ optimizely.track('Submit to Hotlist Gigs');
+ cookies.erase('_tc.hcl');
+ }
+ }
}
});
}
diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx
index 5a939a9442..337bef5414 100644
--- a/src/shared/containers/Gigs/RecruitCRMJobs.jsx
+++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx
@@ -19,7 +19,8 @@ import { getQuery, updateQuery } from 'utils/url';
import { withOptimizely } from '@optimizely/react-sdk';
import './jobLisingStyles.scss';
-const CONTENT_PREVIEW_LENGTH = 175;
+const cookies = require('browser-cookies');
+
const GIGS_PER_PAGE = 10;
// Sort by dropdown
const sortByOptions = [
@@ -47,7 +48,7 @@ class RecruitCRMJobsContainer extends React.Component {
this.onFilter = this.onFilter.bind(this);
this.onLocation = this.onLocation.bind(this);
this.onSort = this.onSort.bind(this);
- this.onHotlistApply = this.onHotlistApply.bind(this);
+ this.onHotlistClick = this.onHotlistClick.bind(this);
}
componentDidMount() {
@@ -121,9 +122,14 @@ class RecruitCRMJobsContainer extends React.Component {
});
}
- onHotlistApply() {
+ onHotlistClick(job) {
const { optimizely } = this.props;
optimizely.track('Hotlist ads click');
+ cookies.set('_tc.hcl', JSON.stringify({
+ slug: job.slug,
+ }), {
+ expires: 0,
+ });
}
render() {
@@ -151,7 +157,8 @@ class RecruitCRMJobsContainer extends React.Component {
// optimizely decide
let decision = { enabled: true };
if (isomorphy.isClientSide()) {
- decision = optimizely.decide('gig_listing_hotlist');
+ // decision = optimizely.decide('gig_listing_hotlist');
+ decision = optimizely.decide('gig_listing_hotlist_center');
}
let jobsToDisplay = jobs;
// build hotlist of jobs if present
@@ -216,6 +223,23 @@ class RecruitCRMJobsContainer extends React.Component {
jobsToDisplay,
page * GIGS_PER_PAGE, (page * GIGS_PER_PAGE) + GIGS_PER_PAGE,
);
+ // hot list of gigs
+ let isHotlistRendered = false;
+ const hotlist = () => (
+
+
+ {
+ hotlistJobs.map((hjob, indx) => (
+
this.onHotlistClick(hjob)}>
+
{hjob.country}
+
{hjob.name}
+
${hjob.min_annual_salary} - {hjob.max_annual_salary} / {getSalaryType(hjob.salary_type)}
+
+ ))
+ }
+
+
+ );
return (
@@ -228,8 +252,27 @@ class RecruitCRMJobsContainer extends React.Component {
{
jobsToDisplay.length
- ? jobsToDisplay.map(job => )
- : No Results
+ ? jobsToDisplay.map((job, indx) => {
+ const featured = getCustomField(job.custom_fields, 'Featured');
+ if ((featured === 'n/a' || indx === 2) && !isHotlistRendered && hotlistJobs.length && decision.enabled) {
+ isHotlistRendered = true;
+ return (
+
+ {hotlist()}
+
+
+ );
+ }
+ return ;
+ })
+ : (
+
+ {
+ hotlistJobs.length && decision.enabled && hotlist()
+ }
+ No Results
+
+ )
}
{
@@ -237,49 +280,6 @@ class RecruitCRMJobsContainer extends React.Component {
?
: null
}
- {
- hotlistJobs.length && decision.enabled && (
-
-
HOT THIS WEEK
-
- {
- hotlistJobs.map((hjob, indx) => (indx <= 1 ? (
-
-
{hjob.country}
-
{hjob.name}
-
${hjob.min_annual_salary} - {hjob.max_annual_salary} / {getSalaryType(hjob.salary_type)}
- {
- getCustomField(hjob.custom_fields, 'Hotlist excerpt') !== 'n/a' ? (
-
- {
- `${getCustomField(hjob.custom_fields, 'Hotlist excerpt').substring(0, CONTENT_PREVIEW_LENGTH)}${getCustomField(hjob.custom_fields, 'Hotlist excerpt').length > CONTENT_PREVIEW_LENGTH ? '...' : ''}`
- }
-
- ) : null
- }
-
- ) : (
-
-
{hjob.country}
-
{hjob.name}
-
${hjob.min_annual_salary} - {hjob.max_annual_salary} / {getSalaryType(hjob.salary_type)}
- {
- getCustomField(hjob.custom_fields, 'Hotlist excerpt') !== 'n/a' ? (
-
- {
- `${getCustomField(hjob.custom_fields, 'Hotlist excerpt').substring(0, CONTENT_PREVIEW_LENGTH)}${getCustomField(hjob.custom_fields, 'Hotlist excerpt').length > CONTENT_PREVIEW_LENGTH ? '...' : ''}`
- }
-
- ) : null
- }
-
Apply Now
-
- )))
- }
-
-
- )
- }
);
}
diff --git a/src/shared/containers/Gigs/_RecruitCRMJobs_ab-v1.jsx b/src/shared/containers/Gigs/_RecruitCRMJobs_ab-v1.jsx
new file mode 100644
index 0000000000..9c5f9bfa7c
--- /dev/null
+++ b/src/shared/containers/Gigs/_RecruitCRMJobs_ab-v1.jsx
@@ -0,0 +1,329 @@
+/**
+ * A block that fetches and renders a job listing page
+ * driven by recruitCRM
+ */
+import _ from 'lodash';
+import actions from 'actions/recruitCRM';
+import LoadingIndicator from 'components/LoadingIndicator';
+import SearchCombo from 'components/GUIKit/SearchCombo';
+import Paginate from 'components/GUIKit/Paginate';
+import JobListCard from 'components/GUIKit/JobListCard';
+import Dropdown from 'components/GUIKit/Dropdown';
+import PT from 'prop-types';
+import React from 'react';
+import { connect } from 'react-redux';
+import { getSalaryType, getCustomField } from 'utils/gigs';
+import IconBlackLocation from 'assets/images/icon-black-location.svg';
+import { config, Link, isomorphy } from 'topcoder-react-utils';
+import { getQuery, updateQuery } from 'utils/url';
+import { withOptimizely } from '@optimizely/react-sdk';
+import './jobLisingStyles.scss';
+
+const cookies = require('browser-cookies');
+
+const CONTENT_PREVIEW_LENGTH = 175;
+const GIGS_PER_PAGE = 10;
+// Sort by dropdown
+const sortByOptions = [
+ { label: 'Latest Added Descending', selected: true },
+ { label: 'Latest Updated Descending', selected: false },
+];
+// Locations
+let locations = [{
+ label: 'All', selected: true,
+}];
+
+class RecruitCRMJobsContainer extends React.Component {
+ constructor(props) {
+ super(props);
+ // Filter initial state
+ this.state = {
+ term: '',
+ page: 0,
+ sortBy: 'created_on',
+ location: 'All',
+ };
+ // binds
+ this.onSearch = this.onSearch.bind(this);
+ this.onPaginate = this.onPaginate.bind(this);
+ this.onFilter = this.onFilter.bind(this);
+ this.onLocation = this.onLocation.bind(this);
+ this.onSort = this.onSort.bind(this);
+ this.onHotlistClick = this.onHotlistClick.bind(this);
+ }
+
+ componentDidMount() {
+ const {
+ getJobs,
+ jobs,
+ } = this.props;
+ const { state } = this;
+ const q = getQuery();
+ // This gets all jobs.
+ // Pagination and filtering on front-side
+ if (!jobs.length) {
+ getJobs({
+ job_status: 1, // Open jobs only
+ });
+ }
+ // handle URL query if present
+ if (q && q.search) {
+ const stateUpdate = {
+ ...state,
+ term: q.search,
+ };
+ this.setState(stateUpdate);
+ }
+ }
+
+ /**
+ * Wraps all calls to setState
+ * @param {Object} newState the state update
+ */
+ onFilter(newState) {
+ // Do updates
+ // update the state
+ this.setState(newState);
+ }
+
+ onSearch(newTerm) {
+ this.onFilter({
+ term: newTerm,
+ page: 0,
+ });
+ // update the URL query
+ updateQuery({
+ search: newTerm,
+ });
+ }
+
+ onPaginate(newPage) {
+ this.onFilter({
+ page: newPage.selected,
+ });
+ window.scrollTo({
+ top: 0,
+ behavior: 'smooth',
+ });
+ }
+
+ onLocation(newLocation) {
+ const selected = _.find(newLocation, { selected: true });
+ this.onFilter({
+ location: selected.label,
+ page: 0,
+ });
+ }
+
+ onSort(newSort) {
+ const selected = _.find(newSort, { selected: true });
+ this.onFilter({
+ sortBy: selected.label === 'Latest Updated Descending' ? 'updated_on' : 'created_on',
+ page: 0,
+ });
+ }
+
+ onHotlistClick(job) {
+ const { optimizely } = this.props;
+ optimizely.track('Hotlist ads click');
+ cookies.set('_tc.hcl', JSON.stringify({
+ slug: job.slug,
+ }), {
+ expires: 0,
+ });
+ }
+
+ render() {
+ const {
+ loading,
+ jobs,
+ optimizely,
+ } = this.props;
+ const {
+ term,
+ page,
+ sortBy,
+ location,
+ } = this.state;
+
+ if (loading) {
+ return (
+
+ ;
+ Searching our database for the best gigsā¦
+
+ );
+ }
+
+ // optimizely decide
+ let decision = { enabled: true };
+ if (isomorphy.isClientSide()) {
+ // decision = optimizely.decide('gig_listing_hotlist');
+ decision = optimizely.decide('gig_listing_hotlist_center');
+ }
+ let jobsToDisplay = jobs;
+ // build hotlist of jobs if present
+ let hotlistJobs = _.filter(jobs, (job) => {
+ const showInHotlist = _.find(job.custom_fields, ['field_name', 'Show in Hotlist']);
+ return showInHotlist && showInHotlist.value;
+ });
+ hotlistJobs = hotlistJobs.sort((a, b) => new Date(b.updated_on) - new Date(a.updated_on));
+ hotlistJobs = _.slice(hotlistJobs, 0, 4);
+ // build current locations dropdown based on all data
+ // and filter by selected location
+ jobsToDisplay = _.filter(jobs, (job) => {
+ const country = job.country === 'Anywhere' || job.country === 'Any' ? 'All' : job.country;
+ // build dropdown
+ const found = _.findIndex(locations, { label: country });
+ if (found === -1) {
+ locations.push({
+ label: country, selected: location.toLowerCase() === country.toLowerCase(),
+ });
+ } else {
+ locations[found].selected = location.toLowerCase() === country.toLowerCase();
+ }
+ locations[0].selected = location === 'All';
+ // filter
+ if (location === 'Anywhere' || location === 'Any' || location === 'All') return true;
+ return location.toLowerCase() === job.country.toLowerCase();
+ });
+ // sort location dropdown
+ locations = _.sortBy(locations, ['label']);
+ // Filter by term
+ if (term) {
+ jobsToDisplay = _.filter(jobsToDisplay, (job) => {
+ // eslint-disable-next-line no-underscore-dangle
+ const _term = term.toLowerCase();
+ // name search
+ if (job.name.toLowerCase().includes(_term)) return true;
+ // skills search
+ const skills = _.find(job.custom_fields, ['field_name', 'Technologies Required']);
+ 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;
+ });
+ }
+ // Sort controlled by sortBy state
+ jobsToDisplay = jobsToDisplay.sort((a, b) => {
+ // sort featured gigs first no matter the sortBy
+ const featuredA = getCustomField(a.custom_fields, 'Featured');
+ const featuredB = getCustomField(b.custom_fields, 'Featured');
+ if (featuredB !== 'n/a' && featuredA === 'n/a') return Number.MAX_VALUE;
+ if (featuredB === 'n/a' && featuredA !== 'n/a') return -Number.MIN_VALUE;
+ return new Date(b[sortBy]) - new Date(a[sortBy]);
+ });
+ // Calc pages
+ const pages = Math.ceil(jobsToDisplay.length / GIGS_PER_PAGE);
+ // Paginate the results
+ jobsToDisplay = _.slice(
+ jobsToDisplay,
+ page * GIGS_PER_PAGE, (page * GIGS_PER_PAGE) + GIGS_PER_PAGE,
+ );
+
+ return (
+
+
+
+
+
+
+
+
+ {
+ jobsToDisplay.length
+ ? jobsToDisplay.map(job => )
+ : No Results
+ }
+
+ {
+ jobsToDisplay.length
+ ?
: null
+ }
+
+ {
+ hotlistJobs.length && decision.enabled && (
+
+
HOT THIS WEEK
+
+ {
+ hotlistJobs.map((hjob, indx) => (indx <= 1 ? (
+
this.onHotlistClick(hjob)}>
+
{hjob.country}
+
{hjob.name}
+
${hjob.min_annual_salary} - {hjob.max_annual_salary} / {getSalaryType(hjob.salary_type)}
+ {
+ getCustomField(hjob.custom_fields, 'Hotlist excerpt') !== 'n/a' ? (
+
+ {
+ `${getCustomField(hjob.custom_fields, 'Hotlist excerpt').substring(0, CONTENT_PREVIEW_LENGTH)}${getCustomField(hjob.custom_fields, 'Hotlist excerpt').length > CONTENT_PREVIEW_LENGTH ? '...' : ''}`
+ }
+
+ ) : null
+ }
+
+ ) : (
+
+
{hjob.country}
+
{hjob.name}
+
${hjob.min_annual_salary} - {hjob.max_annual_salary} / {getSalaryType(hjob.salary_type)}
+ {
+ getCustomField(hjob.custom_fields, 'Hotlist excerpt') !== 'n/a' ? (
+
+ {
+ `${getCustomField(hjob.custom_fields, 'Hotlist excerpt').substring(0, CONTENT_PREVIEW_LENGTH)}${getCustomField(hjob.custom_fields, 'Hotlist excerpt').length > CONTENT_PREVIEW_LENGTH ? '...' : ''}`
+ }
+
+ ) : null
+ }
+
this.onHotlistClick(hjob)}>Apply Now
+
+ )))
+ }
+
+
+ )
+ }
+
+ );
+ }
+}
+
+RecruitCRMJobsContainer.defaultProps = {
+ jobs: [],
+ loading: true,
+};
+
+RecruitCRMJobsContainer.propTypes = {
+ getJobs: PT.func.isRequired,
+ loading: PT.bool,
+ jobs: PT.arrayOf(PT.shape),
+ optimizely: PT.shape().isRequired,
+};
+
+function mapStateToProps(state) {
+ const data = state.recruitCRM;
+ return {
+ jobs: data ? data.jobs : [],
+ loading: data ? data.loading : true,
+ };
+}
+
+function mapDispatchToActions(dispatch) {
+ const a = actions.recruit;
+ return {
+ getJobs: (ownProps) => {
+ dispatch(a.getJobsInit(ownProps));
+ dispatch(a.getJobsDone(ownProps));
+ },
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToActions,
+)(withOptimizely(RecruitCRMJobsContainer));
diff --git a/src/shared/containers/Gigs/_jobLisingStyles_ab-v1.scss b/src/shared/containers/Gigs/_jobLisingStyles_ab-v1.scss
new file mode 100644
index 0000000000..535a92cb16
--- /dev/null
+++ b/src/shared/containers/Gigs/_jobLisingStyles_ab-v1.scss
@@ -0,0 +1,215 @@
+/* stylelint-disable no-descending-specificity */
+@import "~styles/mixins";
+
+.loading-text {
+ font-family: Roboto, sans-serif;
+ font-size: 24px;
+ line-height: 26px;
+ color: #2a2a2a;
+ text-align: center;
+}
+
+.container,
+.container-with-hotlist {
+ max-width: $screen-lg;
+ margin: auto;
+
+ @media (max-width: 1280px) {
+ padding: 0 15px;
+ }
+
+ .gigs {
+ display: block;
+ }
+
+ .filters {
+ display: flex;
+ align-items: flex-end;
+
+ @include xs-to-sm {
+ flex-direction: column;
+ }
+
+ > div {
+ margin-right: 30px;
+ flex: 1;
+
+ @include xs-to-sm {
+ margin-right: 0;
+ margin-bottom: 15px;
+ }
+
+ &:first-child {
+ flex: 3;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+
+ .jobs-list-container {
+ display: flex;
+ flex-direction: column;
+ margin: 20px 0 50px 0;
+
+ .no-results {
+ display: flex;
+ justify-content: center;
+ }
+ }
+}
+
+.container-with-hotlist {
+ display: flex;
+
+ .gigs {
+ width: 956px;
+
+ @media (max-width: 1280px) {
+ max-width: none;
+ flex: 1;
+ }
+ }
+
+ .filters {
+ > div {
+ margin-right: 20px;
+
+ @include xs-to-sm {
+ margin-right: 0;
+ }
+
+ &:nth-child(2) {
+ min-width: 194px;
+ }
+
+ &:last-child {
+ flex: 2;
+ max-width: 223px;
+
+ @include xs-to-sm {
+ max-width: none;
+ }
+ }
+ }
+ }
+
+ .hotlist {
+ display: flex;
+ flex-direction: column;
+ margin-left: 28px;
+ flex: 1;
+
+ @media (max-width: 1280px) {
+ display: none;
+ }
+
+ h5 {
+ font-family: Barlow, sans-serif;
+ font-size: 20px;
+ line-height: 24px;
+ text-transform: uppercase;
+ font-weight: 600;
+ margin: 7px 0 31px 6px;
+ color: #2a2a2a;
+ }
+
+ .hotlist-items {
+ .hotlist-item-1,
+ .hotlist-item-2,
+ .hotlist-item-3,
+ .hotlist-item-4 {
+ display: flex;
+ flex-direction: column;
+ border-radius: 10px;
+ padding: 20px 20px 12px;
+ font-family: Roboto, sans-serif;
+ margin-bottom: 16px;
+ color: #2a2a2a;
+
+ .location {
+ font-size: 14px;
+ display: flex;
+ align-items: center;
+
+ svg {
+ margin-right: 5px;
+ width: 15px;
+ height: 17px;
+ }
+ }
+
+ .job-title {
+ margin: 0;
+ }
+
+ .job-money {
+ line-height: 30px;
+ }
+
+ .job-desc {
+ font-family: Roboto, sans-serif;
+ line-height: 24px;
+ margin-top: 13px;
+ }
+ }
+
+ .hotlist-item-1,
+ .hotlist-item-2,
+ .hotlist-item-4 {
+ color: #fff;
+
+ .location svg g {
+ stroke: #fff;
+ }
+
+ .job-title,
+ .job-desc {
+ color: #fff;
+ }
+ }
+
+ .hotlist-item-1 {
+ background-image: linear-gradient(305.22deg, #9d41c9 0.01%, #ef476f 100%);
+ }
+
+ .hotlist-item-2 {
+ background-image: linear-gradient(140.77deg, #9d41c9 0%, #50ade8 100%);
+ }
+
+ .hotlist-item-3 {
+ background-image: linear-gradient(133.83deg, #f4f4f4 0%, #d4d4d4 100%);
+ }
+
+ .hotlist-item-4 {
+ background-image: linear-gradient(359.14deg, #555 0%, #2a2a2a 100%);
+ }
+
+ .hotlist-item-button-3,
+ .hotlist-item-button-4 {
+ font-family: Roboto, sans-serif;
+ font-size: 12px;
+ letter-spacing: 0.8px;
+ line-height: 30px;
+ padding: 0 15px;
+ text-transform: uppercase;
+ font-weight: bold;
+ margin: 22px 0 9px;
+ max-width: 104px;
+ border-radius: 15px;
+ }
+
+ .hotlist-item-button-3 {
+ background-color: #137d60;
+ color: #fff;
+ }
+
+ .hotlist-item-button-4 {
+ background-color: #fff;
+ color: #229174;
+ }
+ }
+ }
+}
diff --git a/src/shared/containers/Gigs/jobLisingStyles.scss b/src/shared/containers/Gigs/jobLisingStyles.scss
index 535a92cb16..3c294a2fd5 100644
--- a/src/shared/containers/Gigs/jobLisingStyles.scss
+++ b/src/shared/containers/Gigs/jobLisingStyles.scss
@@ -20,6 +20,7 @@
.gigs {
display: block;
+ flex: 1;
}
.filters {
@@ -64,47 +65,10 @@
.container-with-hotlist {
display: flex;
- .gigs {
- width: 956px;
-
- @media (max-width: 1280px) {
- max-width: none;
- flex: 1;
- }
- }
-
- .filters {
- > div {
- margin-right: 20px;
-
- @include xs-to-sm {
- margin-right: 0;
- }
-
- &:nth-child(2) {
- min-width: 194px;
- }
-
- &:last-child {
- flex: 2;
- max-width: 223px;
-
- @include xs-to-sm {
- max-width: none;
- }
- }
- }
- }
-
.hotlist {
display: flex;
- flex-direction: column;
- margin-left: 28px;
flex: 1;
-
- @media (max-width: 1280px) {
- display: none;
- }
+ margin: 5px 0;
h5 {
font-family: Barlow, sans-serif;
@@ -117,6 +81,20 @@
}
.hotlist-items {
+ display: grid;
+ align-items: stretch;
+ gap: 0 20px;
+ width: 100%;
+ grid-template-columns: 1fr 1fr 1fr 1fr;
+
+ @include md-to-lg {
+ grid-template-columns: 1fr 1fr;
+ }
+
+ @include xs-to-sm {
+ grid-template-columns: 1fr;
+ }
+
.hotlist-item-1,
.hotlist-item-2,
.hotlist-item-3,
@@ -133,6 +111,7 @@
font-size: 14px;
display: flex;
align-items: center;
+ line-height: 30px;
svg {
margin-right: 5px;
@@ -176,11 +155,11 @@
}
.hotlist-item-2 {
- background-image: linear-gradient(140.77deg, #9d41c9 0%, #50ade8 100%);
+ background-image: linear-gradient(125.57deg, #2c95d7 0%, #83c5ee 100%);
}
.hotlist-item-3 {
- background-image: linear-gradient(133.83deg, #f4f4f4 0%, #d4d4d4 100%);
+ background-image: linear-gradient(309.43deg, #f4f4f4 0%, #d4d4d4 100%);
}
.hotlist-item-4 {