From c462fa2b9d5fac7acbd049f8e454b71433729114 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 12 Aug 2020 08:43:23 +0300 Subject: [PATCH 01/16] ci: no deploy --- .circleci/config.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index fa18367f07..f73a20e51d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -231,7 +231,6 @@ workflows: branches: only: - develop - - feature-contentful # This is alternate dev env for parallel testing - "build-test": context : org-global @@ -253,7 +252,6 @@ workflows: branches: only: - develop - - feature-contentful # This is stage env for production QA releases - "build-prod-staging": context : org-global From c8ca197b368e70e51bae1085fae6ee909b94f779 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Thu, 13 Aug 2020 15:52:49 +0300 Subject: [PATCH 02/16] recruitCRM API integration --- config/custom-environment-variables.js | 2 + config/default.js | 2 + src/server/index.js | 2 + src/server/routes/recruitCRM.js | 21 ++++++ src/server/services/recruitCRM.js | 73 +++++++++++++++++++ src/shared/actions/recruitCRM.js | 28 +++++++ .../Contentful/AppComponent/index.jsx | 4 + .../containers/Contentful/RecruitCRMJobs.jsx | 73 +++++++++++++++++++ src/shared/reducers/index.js | 2 + src/shared/reducers/recruitCRM.js | 47 ++++++++++++ src/shared/services/recruitCRM.js | 38 ++++++++++ 11 files changed, 292 insertions(+) create mode 100644 src/server/routes/recruitCRM.js create mode 100644 src/server/services/recruitCRM.js create mode 100644 src/shared/actions/recruitCRM.js create mode 100644 src/shared/containers/Contentful/RecruitCRMJobs.jsx create mode 100644 src/shared/reducers/recruitCRM.js create mode 100644 src/shared/services/recruitCRM.js diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index 0dbd155bac..51264f7d73 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -95,6 +95,8 @@ module.exports = { AUTH0_PROXY_SERVER_URL: 'TC_M2M_AUTH0_PROXY_SERVER_URL', TOKEN_CACHE_TIME: 'TOKEN_CACHE_TIME', }, + + RECRUITCRM_API_KEY: 'RECRUITCRM_API_KEY', }, AUTH_CONFIG: { AUTH0_URL: 'TC_M2M_AUTH0_URL', diff --git a/config/default.js b/config/default.js index 483681f0dd..39517766d2 100644 --- a/config/default.js +++ b/config/default.js @@ -247,6 +247,8 @@ module.exports = { AUTH0_URL: '', TOKEN_CACHE_TIME: '', }, + + RECRUITCRM_API_KEY: '', }, AUTH_CONFIG: { diff --git a/src/server/index.js b/src/server/index.js index 062cb1449c..09bde9aa01 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -26,6 +26,7 @@ import { toJson as xmlToJson } from 'utils/xml2json'; import cdnRouter from './routes/cdn'; import mailChimpRouter from './routes/mailchimp'; import mockDocuSignFactory from './__mocks__/docu-sign-mock'; +import recruitCRMRouter from './routes/recruitCRM'; /* Dome API for topcoder communities */ import tcCommunitiesDemoApi from './tc-communities'; @@ -133,6 +134,7 @@ async function onExpressJsSetup(server) { server.use('/api/cdn', cdnRouter); server.use('/api/mailchimp', mailChimpRouter); + server.use('/api/recruit', recruitCRMRouter); // serve demo api server.use( diff --git a/src/server/routes/recruitCRM.js b/src/server/routes/recruitCRM.js new file mode 100644 index 0000000000..a7c035962d --- /dev/null +++ b/src/server/routes/recruitCRM.js @@ -0,0 +1,21 @@ +/** + * The routes related to RecruitCRM.io integration + */ + +import express from 'express'; +import RecruitCRMService from '../services/recruitCRM'; + +const cors = require('cors'); + +const routes = express.Router(); + +// Enables CORS on those routes according config above +// ToDo configure CORS for set of our trusted domains +routes.use(cors()); +routes.options('*', cors()); + +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)); + +export default routes; diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js new file mode 100644 index 0000000000..da95bb85fa --- /dev/null +++ b/src/server/services/recruitCRM.js @@ -0,0 +1,73 @@ +/** + * Server-side functions necessary for effective integration with recruitCRM + */ +import fetch from 'isomorphic-fetch'; +import config from 'config'; +import qs from 'qs'; + +/** + * Auxiliary class that handles communication with recruitCRM + */ +export default class RecruitCRMService { + /** + * Creates a new service instance. + * @param {String} baseUrl The base API endpoint. + */ + constructor(baseUrl = 'https://api.recruitcrm.io') { + this.private = { + baseUrl, + apiKey: config.SECRET.RECRUITCRM_API_KEY, + authorization: `Bearer ${config.SECRET.RECRUITCRM_API_KEY}`, + }; + } + + /** + * Gets jobs endpoint. + * @return {Promise} + * @param {Object} the request. + */ + async getJobs(req, res, next) { + try { + const response = await fetch(`${this.private.baseUrl}/v1/jobs/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.getJobs(req, res, next); + } + const data = await response.json(); + return res.send(data); + } catch (err) { + return next(err); + } + } + + /** + * Gets job by id endpoint. + * @return {Promise} + * @param {Object} the request. + */ + async getJob(req, res, next) { + try { + const response = await fetch(`${this.private.baseUrl}/v1/jobs/${req.params.id}`, { + 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.getJob(req, res, next); + } + const data = await response.json(); + return res.send(data); + } catch (err) { + return next(err); + } + } +} diff --git a/src/shared/actions/recruitCRM.js b/src/shared/actions/recruitCRM.js new file mode 100644 index 0000000000..b85f8fef0e --- /dev/null +++ b/src/shared/actions/recruitCRM.js @@ -0,0 +1,28 @@ +import { redux } from 'topcoder-react-utils'; +import Service from 'services/recruitCRM'; + +/** + * Jobs page fetch init + */ +function getJobsInit() { + return {}; +} + +/** + * Jobs page fetch done + */ +async function getJobsDone(query) { + const ss = new Service(); + const res = await ss.getJobs(query); + + return { + data: res, + }; +} + +export default redux.createActions({ + RECRUIT: { + GET_JOBS_INIT: getJobsInit, + GET_JOBS_DONE: getJobsDone, + }, +}); diff --git a/src/shared/components/Contentful/AppComponent/index.jsx b/src/shared/components/Contentful/AppComponent/index.jsx index ab310304a0..aa60dedf18 100644 --- a/src/shared/components/Contentful/AppComponent/index.jsx +++ b/src/shared/components/Contentful/AppComponent/index.jsx @@ -9,6 +9,7 @@ import PT from 'prop-types'; import React from 'react'; import { errors } from 'topcoder-react-lib'; import Leaderboard from 'containers/tco/Leaderboard'; +import RecruitCRMJobs from 'containers/Contentful/RecruitCRMJobs'; const { fireErrorMessage } = errors; @@ -30,6 +31,9 @@ export function AppComponentSwitch(appComponent) { /> ); } + if (appComponent.fields.type === 'RecruitCRM-Jobs') { + return ; + } fireErrorMessage('Unsupported app component type from contentful', ''); return null; } diff --git a/src/shared/containers/Contentful/RecruitCRMJobs.jsx b/src/shared/containers/Contentful/RecruitCRMJobs.jsx new file mode 100644 index 0000000000..ab9a6a6291 --- /dev/null +++ b/src/shared/containers/Contentful/RecruitCRMJobs.jsx @@ -0,0 +1,73 @@ +/** + * A block that fetches and renders a job listing page + * driven by recruitCRM + */ + +import actions from 'actions/recruitCRM'; +import LoadingIndicator from 'components/LoadingIndicator'; +import PT from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; + +class RecruitCRMJobsContainer extends React.Component { + componentDidMount() { + const { + getJobs, + jobs, + } = this.props; + // Recruit API query stored in state + this.state = { + job_status: 1, // Open jobs + }; + + if (!jobs.data) { + getJobs(this.state); + } + } + + render() { + const { + loading, + jobs, + } = this.props; + + if (loading) { + return ; + } + + return null; + } +} + +RecruitCRMJobsContainer.defaultProps = { + jobs: {}, +}; + +RecruitCRMJobsContainer.propTypes = { + getJobs: PT.func.isRequired, + loading: PT.bool.isRequired, + jobs: PT.shape(), +}; + +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, +)(RecruitCRMJobsContainer); diff --git a/src/shared/reducers/index.js b/src/shared/reducers/index.js index e457d88e62..d1ae5717c9 100644 --- a/src/shared/reducers/index.js +++ b/src/shared/reducers/index.js @@ -36,6 +36,7 @@ import { factory as leaderboardFactory } from './leaderboard'; import { factory as scoreboardFactory } from './tco/scoreboard'; import { factory as termsFactory } from './terms'; import newsletterPreferences from './newsletterPreferences'; +import recruitCRM from './recruitCRM'; /** * Given HTTP request, generates options for SSR by topcoder-react-lib's reducer @@ -140,6 +141,7 @@ export function factory(req) { challengesBlock, policyPages, newsletterPreferences, + recruitCRM, })); } diff --git a/src/shared/reducers/recruitCRM.js b/src/shared/reducers/recruitCRM.js new file mode 100644 index 0000000000..c8fddd97f9 --- /dev/null +++ b/src/shared/reducers/recruitCRM.js @@ -0,0 +1,47 @@ +/** + * Reducer for state.recruit + */ + +import actions from 'actions/recruitCRM'; +import { handleActions } from 'redux-actions'; + +/** + * Handles recruit.getJobsInit action. + * @param {Object} state Previous state. + */ +function onInit(state) { + return { + ...state, + jobs: {}, + loading: true, + }; +} + +/** + * Handles recruit.getJobsDone action. + * @param {Object} state Previous state. + * @param {Object} action The action. + */ +function onDone(state, { payload }) { + return { + ...state, + loading: false, + jobs: payload.data, + }; +} + +/** + * Creates recruitCRM reducer with the specified initial state. + * @param {Object} state Optional. If not given, the default one is + * generated automatically. + * @return {Function} Reducer. + */ +function create(state = {}) { + return handleActions({ + [actions.recruit.getJobsInit]: onInit, + [actions.recruit.getJobsDone]: onDone, + }, state); +} + +/* Reducer with the default initial state. */ +export default create(); diff --git a/src/shared/services/recruitCRM.js b/src/shared/services/recruitCRM.js new file mode 100644 index 0000000000..43b497d923 --- /dev/null +++ b/src/shared/services/recruitCRM.js @@ -0,0 +1,38 @@ +import { config } from 'topcoder-react-utils'; +import fetch from 'isomorphic-fetch'; +import { logger } from 'topcoder-react-lib'; +import qs from 'qs'; + +const LOCAL_MODE = Boolean(config.CONTENTFUL.LOCAL_MODE); + +const PROXY_ENDPOINT = `${LOCAL_MODE ? '' : config.URL.APP}/api/recruit`; + +export default class Service { + baseUrl = PROXY_ENDPOINT; + + /** + * Get jobs by query + * @param {*} query The request query + */ + async getJobs(query) { + const res = await fetch(`${this.baseUrl}/jobs/search?${qs.stringify(query)}`); + if (!res.ok) { + const error = new Error('Failed to get jobs'); + logger.error(error, res); + } + return res.json(); + } + + /** + * Get job by id + * @param {*} id The request id + */ + async getJob(id) { + const res = await fetch(`${this.baseUrl}/jobs/${id}`); + if (!res.ok) { + const error = new Error(`Failed to get job ${id}`); + logger.error(error, res); + } + return res.json(); + } +} From 629b777530e38bc9dd382e98f2d85bbd2871bb74 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Thu, 20 Aug 2020 11:47:03 +0300 Subject: [PATCH 03/16] #4722 fixed data loading issues --- src/shared/actions/leaderboard.js | 4 ++-- .../components/Leaderboard/ChallengeHistoryModal/index.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/actions/leaderboard.js b/src/shared/actions/leaderboard.js index e1216d3301..b379260372 100644 --- a/src/shared/actions/leaderboard.js +++ b/src/shared/actions/leaderboard.js @@ -43,8 +43,8 @@ async function getTcoHistoryChallengesDone(url, competitor) { .then(response => response.json()) .then(jsonResponse => ({ challenges: _.filter(jsonResponse, challenge => ( - challenge['tco_leaderboard.user_id'] - ? (challenge['tco_leaderboard.user_id'] === competitor['tco_leaderboard.user_id']) + challenge['member_profile_basic.user_id'] + ? (challenge['member_profile_basic.user_id'] === competitor['member_profile_basic.user_id']) : (challenge.userid === competitor.userid) )), })); diff --git a/src/shared/components/Leaderboard/ChallengeHistoryModal/index.jsx b/src/shared/components/Leaderboard/ChallengeHistoryModal/index.jsx index 6793da87a5..8ebdbb6725 100644 --- a/src/shared/components/Leaderboard/ChallengeHistoryModal/index.jsx +++ b/src/shared/components/Leaderboard/ChallengeHistoryModal/index.jsx @@ -117,7 +117,7 @@ class ChallengeHistoryModal extends Component { - {challenge['challenge.challenge_name'] || challenge['tco_leaderboard.challenge_id'] || challenge.challenge_id} + {challenge.challenge_name || challenge['challenge.challenge_name'] || challenge['tco_leaderboard.challenge_id'] || challenge.challenge_id} { From 8091bbb21d1326de2ee284dc962df60ca25cbca2 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Tue, 25 Aug 2020 11:04:44 +0300 Subject: [PATCH 04/16] ci: on test --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e2bbcef814..76c90ab694 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -237,7 +237,7 @@ workflows: filters: branches: only: - - integration-v5-challenge-api + - feature-contentful # This is alternate dev env for parallel testing - "build-qa": context : org-global From 730628b84bedbcb0ee3914b6ec40d087e52db78b Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Tue, 25 Aug 2020 16:13:29 +0300 Subject: [PATCH 05/16] Intial code #4788 --- .../Header/__snapshots__/index.jsx.snap | 2 +- config/default.js | 3 +- package-lock.json | 20 +- package.json | 1 + src/assets/images/icon-black-calendar.svg | 36 +++ src/assets/images/icon-black-duration.svg | 26 +++ src/assets/images/icon-black-location.svg | 25 +++ src/assets/images/icon-black-payment.svg | 26 +++ src/assets/images/icon-calendar-gig.svg | 30 +++ src/assets/images/icon-clear-search.svg | 13 ++ src/assets/images/icon-duration.svg | 20 ++ src/assets/images/icon-location.svg | 19 ++ src/assets/images/icon-payment.svg | 18 ++ src/assets/images/icon-skills.svg | 31 +++ src/assets/images/icon-timezone.svg | 20 ++ src/server/routes/recruitCRM.js | 2 + src/server/services/recruitCRM.js | 61 +++++ src/shared/actions/recruitCRM.js | 24 +- .../Contentful/AppComponent/index.jsx | 2 +- .../components/GUIKit/Dropdown/index.jsx | 54 +++++ .../components/GUIKit/Dropdown/style.scss | 27 +++ .../components/GUIKit/JobListCard/index.jsx | 62 +++++ .../components/GUIKit/JobListCard/style.scss | 98 ++++++++ .../components/GUIKit/Paginate/index.jsx | 48 ++++ .../components/GUIKit/Paginate/style.scss | 95 ++++++++ .../components/GUIKit/SearchCombo/index.jsx | 49 ++++ .../components/GUIKit/SearchCombo/style.scss | 65 ++++++ src/shared/components/GUIKit/_default.scss | 2 + src/shared/components/Gigs/GigDetails.jsx | 142 ++++++++++++ src/shared/components/Gigs/style.scss | 212 ++++++++++++++++++ .../containers/Contentful/RecruitCRMJobs.jsx | 73 ------ .../containers/Gigs/RecruitCRMJobDetails.jsx | 69 ++++++ src/shared/containers/Gigs/RecruitCRMJobs.jsx | 153 +++++++++++++ .../containers/Gigs/jobLisingStyles.scss | 20 ++ src/shared/containers/GigsPages.jsx | 41 ++++ src/shared/reducers/recruitCRM.js | 34 ++- src/shared/routes/GigsPages.jsx | 21 ++ src/shared/routes/index.jsx | 6 + src/shared/services/recruitCRM.js | 13 ++ src/shared/utils/gigs.js | 28 +++ 40 files changed, 1606 insertions(+), 85 deletions(-) create mode 100644 src/assets/images/icon-black-calendar.svg create mode 100644 src/assets/images/icon-black-duration.svg create mode 100644 src/assets/images/icon-black-location.svg create mode 100644 src/assets/images/icon-black-payment.svg create mode 100644 src/assets/images/icon-calendar-gig.svg create mode 100644 src/assets/images/icon-clear-search.svg create mode 100644 src/assets/images/icon-duration.svg create mode 100644 src/assets/images/icon-location.svg create mode 100644 src/assets/images/icon-payment.svg create mode 100644 src/assets/images/icon-skills.svg create mode 100644 src/assets/images/icon-timezone.svg create mode 100644 src/shared/components/GUIKit/Dropdown/index.jsx create mode 100644 src/shared/components/GUIKit/Dropdown/style.scss create mode 100644 src/shared/components/GUIKit/JobListCard/index.jsx create mode 100644 src/shared/components/GUIKit/JobListCard/style.scss create mode 100644 src/shared/components/GUIKit/Paginate/index.jsx create mode 100644 src/shared/components/GUIKit/Paginate/style.scss create mode 100644 src/shared/components/GUIKit/SearchCombo/index.jsx create mode 100644 src/shared/components/GUIKit/SearchCombo/style.scss create mode 100644 src/shared/components/GUIKit/_default.scss create mode 100644 src/shared/components/Gigs/GigDetails.jsx create mode 100644 src/shared/components/Gigs/style.scss delete mode 100644 src/shared/containers/Contentful/RecruitCRMJobs.jsx create mode 100644 src/shared/containers/Gigs/RecruitCRMJobDetails.jsx create mode 100644 src/shared/containers/Gigs/RecruitCRMJobs.jsx create mode 100644 src/shared/containers/Gigs/jobLisingStyles.scss create mode 100644 src/shared/containers/GigsPages.jsx create mode 100644 src/shared/routes/GigsPages.jsx create mode 100644 src/shared/utils/gigs.js diff --git a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap index 9ef50e5635..9f0f7a4186 100644 --- a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap @@ -62,7 +62,7 @@ exports[`Default render 1`] = ` "title": "Competitive Programming", }, Object { - "href": "/community/taas", + "href": "/gigs", "title": "Gig Work", }, Object { diff --git a/config/default.js b/config/default.js index 39517766d2..e9c1df07aa 100644 --- a/config/default.js +++ b/config/default.js @@ -314,7 +314,7 @@ module.exports = { }, { title: 'Gig Work', - href: '/community/taas', + href: '/gigs', }, { title: 'Practice', @@ -408,4 +408,5 @@ module.exports = { TC_EDU_SEARCH_PATH: '/search', TC_EDU_SEARCH_BAR_MAX_RESULTS_EACH_GROUP: 3, POLICY_PAGES_PATH: '/policy', + GIGS_PAGES_PATH: '/gigs', }; diff --git a/package-lock.json b/package-lock.json index 7ffac5e8f9..e1382e2e11 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18331,6 +18331,14 @@ "prop-types": "^15.7.2" } }, + "react-paginate": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-6.3.2.tgz", + "integrity": "sha512-Ch++Njfv8UHpLtIMiQouAPeJQA5Ki86kIYfCer6c1B96Rvn3UF27se+goCilCP8oHNXNsA2R2kxvzanY1YIkyg==", + "requires": { + "prop-types": "^15.6.1" + } + }, "react-player": { "version": "0.24.6", "resolved": "https://registry.npmjs.org/react-player/-/react-player-0.24.6.tgz", @@ -33196,9 +33204,9 @@ "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, "topcoder-react-lib": { - "version": "1000.19.44", - "resolved": "https://registry.npmjs.org/topcoder-react-lib/-/topcoder-react-lib-1000.19.44.tgz", - "integrity": "sha512-WoBJbt5w50Hdho9xCzUFwCL/JOQLE0mfMCY3Y0YMUNAToieDpE2RloOwHZpqez+QgE1sxehLGQxlf61M9NZ95A==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/topcoder-react-lib/-/topcoder-react-lib-1.0.0.tgz", + "integrity": "sha512-JomJKRPAEKPTgcSiCapQe/o2f4jUuW2K5X61FT47OSDVQj7HbKoXZ1cM814Nn/x80wwniFQ71czdFiSz0aYGMQ==", "requires": { "auth0-js": "^6.8.4", "config": "^3.2.0", @@ -33428,9 +33436,9 @@ } }, "topcoder-react-ui-kit": { - "version": "1000.0.4", - "resolved": "https://registry.npmjs.org/topcoder-react-ui-kit/-/topcoder-react-ui-kit-1000.0.4.tgz", - "integrity": "sha512-VvVvrVPhcnaJYLNv3YJLIY+CI+4sLLrhdqfulngMmTcUZft01BUorhMYEBtwhODrfotnx48GXnxu1TQmI3L6dA==", + "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==", "requires": { "prop-types": "^15.6.2", "react": "^16.4.1", diff --git a/package.json b/package.json index e8542c7a64..3c9ed33fbe 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "react-infinite-scroller": "^1.1.4", "react-inlinesvg": "^0.8.4", "react-markdown": "^4.3.1", + "react-paginate": "^6.3.2", "react-player": "^0.24.1", "react-redux": "^5.0.7", "react-redux-toastr": "^7.2.6", diff --git a/src/assets/images/icon-black-calendar.svg b/src/assets/images/icon-black-calendar.svg new file mode 100644 index 0000000000..5833517734 --- /dev/null +++ b/src/assets/images/icon-black-calendar.svg @@ -0,0 +1,36 @@ + + + + E4093E96-1304-4DDD-B1C6-AAD5B3E791FD + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-black-duration.svg b/src/assets/images/icon-black-duration.svg new file mode 100644 index 0000000000..d9e8d83053 --- /dev/null +++ b/src/assets/images/icon-black-duration.svg @@ -0,0 +1,26 @@ + + + + E108B11C-D7C2-408D-B020-088D167D4630 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-black-location.svg b/src/assets/images/icon-black-location.svg new file mode 100644 index 0000000000..e8335f35f2 --- /dev/null +++ b/src/assets/images/icon-black-location.svg @@ -0,0 +1,25 @@ + + + + E0744452-EE76-4789-8A85-0562AC9A587C + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-black-payment.svg b/src/assets/images/icon-black-payment.svg new file mode 100644 index 0000000000..16883c87b0 --- /dev/null +++ b/src/assets/images/icon-black-payment.svg @@ -0,0 +1,26 @@ + + + + 8918DD34-77B4-4058-BE0F-975E843D7D81 + Created with sketchtool. + + + + + + + + + + + $ + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-calendar-gig.svg b/src/assets/images/icon-calendar-gig.svg new file mode 100644 index 0000000000..14b2544617 --- /dev/null +++ b/src/assets/images/icon-calendar-gig.svg @@ -0,0 +1,30 @@ + + + + 7AC9DEAC-3F2A-487E-B616-A085E6B4D7FA + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-clear-search.svg b/src/assets/images/icon-clear-search.svg new file mode 100644 index 0000000000..228051d680 --- /dev/null +++ b/src/assets/images/icon-clear-search.svg @@ -0,0 +1,13 @@ + + + + E008ABA4-32F2-42CD-9934-378FD0AB461B + Created with sketchtool. + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-duration.svg b/src/assets/images/icon-duration.svg new file mode 100644 index 0000000000..3eeeb3eddb --- /dev/null +++ b/src/assets/images/icon-duration.svg @@ -0,0 +1,20 @@ + + + + 52C90FFB-CC27-4B10-8E63-415ACA6557E4 + Created with sketchtool. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-location.svg b/src/assets/images/icon-location.svg new file mode 100644 index 0000000000..a63b486f66 --- /dev/null +++ b/src/assets/images/icon-location.svg @@ -0,0 +1,19 @@ + + + + AAEC0EB0-9F48-4361-A4E3-F440FA4B8F40 + Created with sketchtool. + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-payment.svg b/src/assets/images/icon-payment.svg new file mode 100644 index 0000000000..c0eeee663e --- /dev/null +++ b/src/assets/images/icon-payment.svg @@ -0,0 +1,18 @@ + + + + D27E7EE2-D0BE-43EA-B61C-C22ADE2FA375 + Created with sketchtool. + + + + + + + $ + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-skills.svg b/src/assets/images/icon-skills.svg new file mode 100644 index 0000000000..e2c6de4376 --- /dev/null +++ b/src/assets/images/icon-skills.svg @@ -0,0 +1,31 @@ + + + + 52CD6D74-4265-497A-89D8-FF91F35564F8 + Created with sketchtool. + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-timezone.svg b/src/assets/images/icon-timezone.svg new file mode 100644 index 0000000000..39158cee60 --- /dev/null +++ b/src/assets/images/icon-timezone.svg @@ -0,0 +1,20 @@ + + + + 90608BD4-776C-4ABD-B264-60B29B9673AC + Created with sketchtool. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/server/routes/recruitCRM.js b/src/server/routes/recruitCRM.js index a7c035962d..5047dcdb0d 100644 --- a/src/server/routes/recruitCRM.js +++ b/src/server/routes/recruitCRM.js @@ -14,6 +14,8 @@ const routes = express.Router(); 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)); diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js index da95bb85fa..20f3e35b63 100644 --- a/src/server/services/recruitCRM.js +++ b/src/server/services/recruitCRM.js @@ -4,6 +4,7 @@ import fetch from 'isomorphic-fetch'; import config from 'config'; import qs from 'qs'; +import _ from 'lodash'; /** * Auxiliary class that handles communication with recruitCRM @@ -39,6 +40,12 @@ export default class RecruitCRMService { await new Promise(resolve => setTimeout(resolve, 30000)); // wait 30sec return this.getJobs(req, res, next); } + if (response.status >= 400) { + return res.send({ + error: true, + status: response.status, + }); + } const data = await response.json(); return res.send(data); } catch (err) { @@ -64,10 +71,64 @@ export default class RecruitCRMService { await new Promise(resolve => setTimeout(resolve, 30000)); // wait 30sec return this.getJob(req, res, next); } + if (response.status >= 400) { + return res.send({ + error: true, + status: response.status, + }); + } const data = await response.json(); return res.send(data); } catch (err) { return next(err); } } + + /** + * Gets all jobs endpoint. + * @return {Promise} + * @param {Object} the request. + */ + async getAllJobs(req, res, next) { + try { + const response = await fetch(`${this.private.baseUrl}/v1/jobs/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.getJobs(req, res, next); + } + if (response.status >= 400) { + return res.send({ + error: true, + status: response.status, + }); + } + const data = await response.json(); + if (data.current_page < data.last_page) { + const pages = _.range(2, data.last_page + 1); + // eslint-disable-next-line no-restricted-syntax + for (const page of pages) { + // eslint-disable-next-line no-await-in-loop + const pageDataRsp = await fetch(`${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}&page=${page}`, { + method: 'GET', + headers: { + 'Content-Type': req.headers['content-type'], + Authorization: this.private.authorization, + }, + }); + // eslint-disable-next-line no-await-in-loop + const pageData = await pageDataRsp.json(); + data.data = _.flatten(data.data.concat(pageData.data)); + } + } + return res.send(data.data); + } catch (err) { + return next(err); + } + } } diff --git a/src/shared/actions/recruitCRM.js b/src/shared/actions/recruitCRM.js index b85f8fef0e..670b71878a 100644 --- a/src/shared/actions/recruitCRM.js +++ b/src/shared/actions/recruitCRM.js @@ -13,16 +13,38 @@ function getJobsInit() { */ async function getJobsDone(query) { const ss = new Service(); - const res = await ss.getJobs(query); + const res = await ss.getAllJobs(query); return { data: res, }; } +/** + * Job fetch init + */ +function getJobInit(id) { + return { id }; +} + +/** + * Job fetch done + */ +async function getJobDone(id) { + const ss = new Service(); + const res = await ss.getJob(id); + + 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, }, }); diff --git a/src/shared/components/Contentful/AppComponent/index.jsx b/src/shared/components/Contentful/AppComponent/index.jsx index aa60dedf18..5238ae7638 100644 --- a/src/shared/components/Contentful/AppComponent/index.jsx +++ b/src/shared/components/Contentful/AppComponent/index.jsx @@ -9,7 +9,7 @@ import PT from 'prop-types'; import React from 'react'; import { errors } from 'topcoder-react-lib'; import Leaderboard from 'containers/tco/Leaderboard'; -import RecruitCRMJobs from 'containers/Contentful/RecruitCRMJobs'; +import RecruitCRMJobs from 'containers/Gigs/RecruitCRMJobs'; const { fireErrorMessage } = errors; diff --git a/src/shared/components/GUIKit/Dropdown/index.jsx b/src/shared/components/GUIKit/Dropdown/index.jsx new file mode 100644 index 0000000000..605f542276 --- /dev/null +++ b/src/shared/components/GUIKit/Dropdown/index.jsx @@ -0,0 +1,54 @@ +/* eslint-disable jsx-a11y/label-has-for */ +/** + * Dropdown component. + */ +import React from 'react'; +import PT from 'prop-types'; +import ReactSelect from 'react-select'; +import './style.scss'; + +function Dropdown({ + options, + selectedId, + placeholder, + label, + onChange, +}) { + return ( +
+ + ( + + {option.name} + + )} + /> +
+ ); +} + +Dropdown.defaultProps = { + options: [], + selectedId: null, + placeholder: '', + label: '', +}; + +Dropdown.propTypes = { + options: PT.arrayOf(PT.shape), + selectedId: PT.string, + placeholder: PT.string, + label: PT.string, + onChange: PT.func.isRequired, +}; + +export default Dropdown; diff --git a/src/shared/components/GUIKit/Dropdown/style.scss b/src/shared/components/GUIKit/Dropdown/style.scss new file mode 100644 index 0000000000..01dc2ec294 --- /dev/null +++ b/src/shared/components/GUIKit/Dropdown/style.scss @@ -0,0 +1,27 @@ +.container { + :global { + @import '~react-select/dist/react-select'; + + width: 100%; + + input.Select-input, + input.Select-input:focus { + background-color: transparent !important; + margin-left: 0 !important; + padding-right: 6px !important; + color: red; + } + + .Select-multi-value-wrapper { + width: 100% !important; + } + } + + .label { + position: relative; + } + + .active-option { + color: red; + } +} diff --git a/src/shared/components/GUIKit/JobListCard/index.jsx b/src/shared/components/GUIKit/JobListCard/index.jsx new file mode 100644 index 0000000000..52316f8091 --- /dev/null +++ b/src/shared/components/GUIKit/JobListCard/index.jsx @@ -0,0 +1,62 @@ +/* eslint-disable max-len */ +/** + * SearchCombo component. + */ +import React from 'react'; +import PT from 'prop-types'; +import { config, Link } from 'topcoder-react-utils'; +import { getSalaryType, getCustomField } from 'utils/gigs'; +import './style.scss'; +import IconBlackCalendar from 'assets/images/icon-black-calendar.svg'; +import IconBlackDuration from 'assets/images/icon-black-duration.svg'; +import IconBlackLocation from 'assets/images/icon-black-location.svg'; +import IconBlackPayment from 'assets/images/icon-black-payment.svg'; +import IconBlackSkills from 'assets/images/icon-skills.svg'; + +export default function JobListCard({ + job, +}) { + let skills = getCustomField(job.custom_fields, 'Technologies Required'); + if (skills !== 'n/a') { + skills = skills.split(','); + if (skills.length > 2) { + skills = `${skills.slice(0, 2).join(', ')},...`; + } else { + skills = skills.join(', '); + } + } + const hoursPerWeek = getCustomField(job.custom_fields, 'Hours per week'); + return ( +
+
{job.name}
+
+
+ {skills} +
+
+ {job.country} +
+
+ ${job.min_annual_salary} - ${job.max_annual_salary} / {getSalaryType(job.salary_type)} +
+
+ {getCustomField(job.custom_fields, 'Duration')} +
+
+ {`${hoursPerWeek !== 'n/a' ? `${hoursPerWeek} hours / week` : 'n/a'}`} +
+
+ VIEW DETAILS +
+
+
+ ); +} + +JobListCard.defaultProps = { + +}; + +JobListCard.propTypes = { + job: PT.shape().isRequired, +}; diff --git a/src/shared/components/GUIKit/JobListCard/style.scss b/src/shared/components/GUIKit/JobListCard/style.scss new file mode 100644 index 0000000000..63ebb264ea --- /dev/null +++ b/src/shared/components/GUIKit/JobListCard/style.scss @@ -0,0 +1,98 @@ +@import "~components/GUIKit/default"; +@import "~components/Contentful/default"; + +.container { + border: 1px solid #e9e9e9; + border-radius: 10px; + display: flex; + flex-direction: column; + color: #2a2a2a; + padding: 25px 35px; + margin-bottom: 15px; + + @include gui-kit-headers; + @include gui-kit-content; + @include roboto-regular; + + h6 { + color: #1e94a3; + margin-top: 0; + margin-bottom: 12px; + + @include xs-to-sm { + margin-bottom: 20px; + } + } + + .job-infos { + display: flex; + + @include xs-to-sm { + flex-direction: column; + } + + .icon-val { + display: flex; + align-items: center; + + @include xs-to-sm { + margin-bottom: 20px; + } + + &:first-child { + width: 200px; + } + + &:nth-child(2) { + width: 204px; + } + + &:nth-child(3) { + width: 263px; + } + + &:nth-child(4) { + width: 195px; + } + + &:last-child { + margin-right: 0; + width: 141px; + } + + svg { + margin-right: 7px; + } + } + + .row-btn { + display: flex; + justify-content: flex-end; + flex: 1; + + @include xs-to-sm { + justify-content: flex-start; + } + + button.primary-green-md { + outline: none; + + @include primary-green; + @include md; + + &:hover { + @include primary-green; + } + + &:disabled, + &:hover:disabled { + background-color: #e9e9e9 !important; + border: none !important; + text-decoration: none !important; + color: #fafafb !important; + box-shadow: none !important; + } + } + } + } +} diff --git a/src/shared/components/GUIKit/Paginate/index.jsx b/src/shared/components/GUIKit/Paginate/index.jsx new file mode 100644 index 0000000000..7dc2eb3c8d --- /dev/null +++ b/src/shared/components/GUIKit/Paginate/index.jsx @@ -0,0 +1,48 @@ +/** + * Paginate component. + * Based on https://github.com/AdeleD/react-paginate + */ +import React from 'react'; +import ReactPaginate from 'react-paginate'; +import PT from 'prop-types'; +import './style.scss'; + +function Paginate({ + pages, + page, + onChange, +}) { + return ( +
+ +
+ ); +} + +Paginate.defaultProps = { + page: 1, +}; + +Paginate.propTypes = { + pages: PT.number.isRequired, + page: PT.number, + onChange: PT.func.isRequired, +}; + +export default Paginate; diff --git a/src/shared/components/GUIKit/Paginate/style.scss b/src/shared/components/GUIKit/Paginate/style.scss new file mode 100644 index 0000000000..70a82393b3 --- /dev/null +++ b/src/shared/components/GUIKit/Paginate/style.scss @@ -0,0 +1,95 @@ +@import "~components/GUIKit/default"; + +.container { + display: flex; + justify-content: center; + + :global { + .paginator { + display: flex; + + @include xs-to-sm { + padding: 0 15px; + } + } + + .previous-link, + .next-link { + border-radius: 15px; + width: 77px; + height: 30px; + margin: 0; + border: 1px solid #137d60; + color: #229174; + font-family: Roboto, sans-serif; + font-size: 12px; + font-weight: bold; + outline: none; + padding: 0; + display: flex; + justify-content: center; + align-items: center; + cursor: pointer; + } + + .previous-link { + margin-right: 5px; + + &::before { + content: '\02039'; + font-size: 28px; + padding-bottom: 5px; + margin-right: 4px; + } + } + + .next-link { + margin-left: 5px; + + &::after { + content: '\0203A'; + font-size: 28px; + padding-bottom: 5px; + margin-left: 4px; + } + } + + .paginator-btn { + background-color: #fff; + border: 1px solid #137d60; + color: #229174; + height: 30px; + width: 30px; + font-size: 12px; + font-family: Roboto, sans-serif; + font-weight: bold; + outline: none; + padding: 0; + border-radius: 100%; + margin: 0 5px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + } + + .paginator-btn-active { + color: #2a2a2a; + border: 1px solid #555; + } + + .paginator-break-link { + color: #d4d4d4; + display: flex; + align-items: center; + justify-content: center; + height: 30px; + font-weight: bolder; + margin: 0 5px; + padding-bottom: 11px; + cursor: pointer; + outline: none; + font-size: 22px; + } + } +} diff --git a/src/shared/components/GUIKit/SearchCombo/index.jsx b/src/shared/components/GUIKit/SearchCombo/index.jsx new file mode 100644 index 0000000000..2ec937621e --- /dev/null +++ b/src/shared/components/GUIKit/SearchCombo/index.jsx @@ -0,0 +1,49 @@ +/** + * SearchCombo component. + */ +import React, { useState } from 'react'; +import PT from 'prop-types'; +import './style.scss'; +import IconClearSearch from 'assets/images/icon-clear-search.svg'; + +function SearchCombo({ + term, + placeholder, + btnText, + onSearch, +}) { + const [inputVal, setVal] = useState(term); + const clearSearch = () => { + setVal(''); + onSearch(''); + }; + + return ( +
+
+ setVal(event.target.value)} /> + { + inputVal ? : null + } +
+ +
+ ); +} + +SearchCombo.defaultProps = { + term: '', + placeholder: '', + btnText: 'SEARCH', +}; + +SearchCombo.propTypes = { + term: PT.string, + placeholder: PT.string, + btnText: PT.string, + onSearch: PT.func.isRequired, +}; + +export default SearchCombo; diff --git a/src/shared/components/GUIKit/SearchCombo/style.scss b/src/shared/components/GUIKit/SearchCombo/style.scss new file mode 100644 index 0000000000..4f25854885 --- /dev/null +++ b/src/shared/components/GUIKit/SearchCombo/style.scss @@ -0,0 +1,65 @@ +@import "~components/GUIKit/default"; + +.container { + display: flex; + align-items: center; + width: 100%; + + .input-wrap { + width: 100%; + position: relative; + margin-right: 10px; + + input.input { + background-color: #fff; + border: 1px solid #aaa; + border-radius: 6px; + height: 39px; + margin: 0; + + &::placeholder { + color: #aaa; + font-size: 14px; + line-height: 22px; + text-transform: none; + } + } + + .clear-search { + position: absolute; + top: calc(50% - 5px); + right: 15px; + } + } + + button.primary-green-md { + outline: none; + display: flex; + align-items: center; + + @include primary-green; + @include md; + + &:hover { + @include primary-green; + } + + &:disabled, + &:hover:disabled { + background-color: #e9e9e9 !important; + border: none !important; + text-decoration: none !important; + color: #fafafb !important; + box-shadow: none !important; + } + + &::before { + content: ''; + background-image: url("data:image/svg+xml,%3C%3Fxml version='1.0' encoding='UTF-8'%3F%3E%3Csvg width='16px' height='16px' viewBox='0 0 16 16' version='1.1' xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'%3E%3C!-- Generator: sketchtool 63.1 (101010) - https://sketch.com --%3E%3Ctitle%3E8A0513C9-B831-4C33-8BAA-A13D6D42B150%3C/title%3E%3Cdesc%3ECreated with sketchtool.%3C/desc%3E%3Cdefs%3E%3Cpath d='M12.7,11.2298137 C13.6,10.0372671 14.1,8.64596273 14.1,7.05590062 C14.1,3.18012422 11,0 7.1,0 C3.2,0 0,3.18012422 0,7.05590062 C0,10.931677 3.2,14.1118012 7.1,14.1118012 C8.7,14.1118012 10.2,13.6149068 11.3,12.7204969 L14.3,15.7018634 C14.5,15.9006211 14.8,16 15,16 C15.2,16 15.5,15.9006211 15.7,15.7018634 C16.1,15.3043478 16.1,14.7080745 15.7,14.310559 L12.7,11.2298137 Z M7.1,12.0248447 C4.3,12.0248447 2,9.83850932 2,7.05590062 C2,4.27329193 4.3,1.98757764 7.1,1.98757764 C9.9,1.98757764 12.2,4.27329193 12.2,7.05590062 C12.2,9.83850932 9.9,12.0248447 7.1,12.0248447 L7.1,12.0248447 Z' id='path-1'%3E%3C/path%3E%3C/defs%3E%3Cg id='TaaS' stroke='none' stroke-width='1' fill='none' fill-rule='evenodd'%3E%3Cg id='02-2-Gig-Work-Listing-Page-2----Approved' transform='translate(-773.000000, -308.000000)'%3E%3Cg id='UI-Kit/Button/Medium/primary' transform='translate(753.000000, 296.000000)'%3E%3Cg id='button-md'%3E%3Cg id='Stacked-Group' transform='translate(20.000000, 0.000000)'%3E%3Cg id='UI-Kit/Icons/magnifying-glass/normal' transform='translate(0.000000, 12.000000)'%3E%3Cmask id='mask-2' fill='white'%3E%3Cuse xlink:href='%23path-1'%3E%3C/use%3E%3C/mask%3E%3Cuse id='icon-color' fill='%23FFFFFF' fill-rule='evenodd' xlink:href='%23path-1'%3E%3C/use%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/g%3E%3C/svg%3E") !important; + background-repeat: no-repeat; + width: 16px; + height: 16px; + margin-right: 5px; + } + } +} diff --git a/src/shared/components/GUIKit/_default.scss b/src/shared/components/GUIKit/_default.scss new file mode 100644 index 0000000000..b88b18f6e3 --- /dev/null +++ b/src/shared/components/GUIKit/_default.scss @@ -0,0 +1,2 @@ +@import "~components/Contentful/default"; +@import "~components/buttons/themed/tc"; diff --git a/src/shared/components/Gigs/GigDetails.jsx b/src/shared/components/Gigs/GigDetails.jsx new file mode 100644 index 0000000000..faa587e607 --- /dev/null +++ b/src/shared/components/Gigs/GigDetails.jsx @@ -0,0 +1,142 @@ +/* eslint-disable max-len */ +/** + * The Gig details component. + */ + +import React from 'react'; +import PT from 'prop-types'; +import { isomorphy, Link, config } from 'topcoder-react-utils'; +import ReactHtmlParser from 'react-html-parser'; +import { getSalaryType, getCustomField } from 'utils/gigs'; +import './style.scss'; +import IconFacebook from 'assets/images/icon-facebook.svg'; +import IconTwitter from 'assets/images/icon-twitter.svg'; +import IconLinkedIn from 'assets/images/icon-linkedIn.svg'; +import IconLocation from 'assets/images/icon-location.svg'; +import IconMoney from 'assets/images/icon-payment.svg'; +import IconDuration from 'assets/images/icon-calendar-gig.svg'; +import IconHours from 'assets/images/icon-duration.svg'; +import IconTimezone from 'assets/images/icon-timezone.svg'; + +// Cleanup HTML from style tags +// so it won't affect other parts of the UI +const ReactHtmlParserOptions = { + // eslint-disable-next-line consistent-return + transform: (node) => { + if (node.type === 'style' && node.name === 'style') { + return null; + } + }, +}; + +export default function GigDetails(props) { + const { job } = props; + let shareUrl; + if (isomorphy.isClientSide()) { + shareUrl = encodeURIComponent(window.location.href); + } + + return ( +
+ { + job.error ? ( +
+

Gig does not exist.

+
+ VIEW OTHER JOBS +
+
+ ) : ( +
+

{job.name}

+
+
+ +
+ Location + {job.country} +
+
+
+ +
+ Compensation + ${job.min_annual_salary} - ${job.max_annual_salary} / {getSalaryType(job.salary_type)} +
+
+
+ +
+ Duration + {getCustomField(job.custom_fields, 'Duration')} +
+
+
+ +
+ Hours + {getCustomField(job.custom_fields, 'Hours per week')} hours / week +
+
+
+ +
+ Timezone + {getCustomField(job.custom_fields, 'Timezone')} +
+
+
+
+
+

Description

+

{ReactHtmlParser(job.job_description_text, ReactHtmlParserOptions)} +

+

Notes

+
+ + * Topcoder does not provide visa sponsorship nor will we work with Staffing Agencies. + + + ** Topcoder and Wipro employees are not eligible for Gig work opportunities. Do not apply and send questions to support@topcoder.com. + +
+
+ APPLY TO THIS JOB + VIEW OTHER JOBS +
+
+
+
+ Share this job on:   + + + + + + + + + +
+
+

Thank you for checking out our latest gig at Topcoder. Gig work through us is simple and effective for those that would like traditional freelance work. To learn more about how Gigs work with us, go here.

+

At Topcoder, we pride ourselves in bringing our customers the very best candidates to help fill their needs. Want to improve your chances? You can do a few things:

+
    +
  • Check out our Topcoder challenges and participate. Challenges showing your technology skills make you a “qualified” candidate so we know you’re good. The proof is in the pudding!
  • +
  • Make sure your Topcoder profile says it all. Fill out your profile to the best of your ability. Your skills, your location, your devices, etc, all help you improve your chances of being selected for a gig
  • +
  • Let us know you’re here! Check in on our Gig Work forum and tell us you’re looking for a gig. It’s great visibility for the Gig team
  • +
  • Subscribe to our Gig notifications email. We’ll send you a weekly update on gigs available so you don’t miss a beat. Find the button at the top of this page.
  • +
+
+
+
+
+ ) + } +
+ ); +} + +GigDetails.propTypes = { + job: PT.shape().isRequired, +}; diff --git a/src/shared/components/Gigs/style.scss b/src/shared/components/Gigs/style.scss new file mode 100644 index 0000000000..5c4ea6b989 --- /dev/null +++ b/src/shared/components/Gigs/style.scss @@ -0,0 +1,212 @@ +@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; + } + + .wrap { + h2 { + color: #26b3c5; + text-align: center; + margin-top: 47px; + margin-bottom: 40px; + + @include xs-to-md { + text-align: left; + } + } + + .infos { + display: flex; + justify-content: center; + border-bottom: 1px solid #e9e9e9; + padding-bottom: 52px; + + @include xs-to-md { + flex-direction: column; + } + + .infos-item { + display: flex; + margin-right: 70px; + + @include xs-to-md { + margin-bottom: 15px; + } + + &:last-child { + margin-right: 0; + } + + svg { + margin-right: 8px; + margin-top: 6px; + } + + .infos-data { + display: flex; + flex-direction: column; + + strong { + font-weight: bold; + line-height: 30px; + } + } + } + } + + .content { + display: flex; + margin-top: 44px; + margin-bottom: 192px; + + @include xs-to-md { + flex-direction: column; + } + + .right { + min-width: 408px; + display: flex; + flex-direction: column; + flex: 1; + + @include xs-to-md { + margin-top: 47px; + } + + .shareButtons { + display: flex; + align-items: center; + + a { + margin-right: 5px; + + &:last-child { + margin-right: 0; + } + } + } + + .info-area { + background-color: #f4f4f4; + padding: 20px; + border-radius: 10px; + margin-top: 18px; + + /* stylelint-disable */ + p, + a, + li { + font-size: 14px; + line-height: 26px; + } + + /* stylelint-enable */ + + ul { + margin-bottom: 0; + + li { + margin-bottom: 15px; + + &:last-child { + margin-bottom: 0; + } + } + } + } + } + + .left { + flex: 2; + margin-right: 80px; + + /* stylelint-disable */ + div strong { + font-weight: bold; + line-height: 30px; + display: inline-block; + } + + /* stylelint-enable */ + } + } + } + + .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 */ + + .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-right: 20px; + + &:hover { + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2); + background-color: #0ab88a; + } + + @include xs-to-sm { + margin-bottom: 20px; + } + } + } +} diff --git a/src/shared/containers/Contentful/RecruitCRMJobs.jsx b/src/shared/containers/Contentful/RecruitCRMJobs.jsx deleted file mode 100644 index ab9a6a6291..0000000000 --- a/src/shared/containers/Contentful/RecruitCRMJobs.jsx +++ /dev/null @@ -1,73 +0,0 @@ -/** - * A block that fetches and renders a job listing page - * driven by recruitCRM - */ - -import actions from 'actions/recruitCRM'; -import LoadingIndicator from 'components/LoadingIndicator'; -import PT from 'prop-types'; -import React from 'react'; -import { connect } from 'react-redux'; - -class RecruitCRMJobsContainer extends React.Component { - componentDidMount() { - const { - getJobs, - jobs, - } = this.props; - // Recruit API query stored in state - this.state = { - job_status: 1, // Open jobs - }; - - if (!jobs.data) { - getJobs(this.state); - } - } - - render() { - const { - loading, - jobs, - } = this.props; - - if (loading) { - return ; - } - - return null; - } -} - -RecruitCRMJobsContainer.defaultProps = { - jobs: {}, -}; - -RecruitCRMJobsContainer.propTypes = { - getJobs: PT.func.isRequired, - loading: PT.bool.isRequired, - jobs: PT.shape(), -}; - -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, -)(RecruitCRMJobsContainer); diff --git a/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx new file mode 100644 index 0000000000..b69aae2efe --- /dev/null +++ b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx @@ -0,0 +1,69 @@ +/** + * A block that fetches and renders a job details page + * driven by recruitCRM + */ + +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'; + +class RecruitCRMJobDetailsContainer extends React.Component { + componentDidMount() { + const { + getJob, + id, + } = this.props; + + getJob(id); + } + + render() { + const { + loading, + job, + } = this.props; + + if (loading) { + return ; + } + + return ; + } +} + +RecruitCRMJobDetailsContainer.defaultProps = { + job: {}, +}; + +RecruitCRMJobDetailsContainer.propTypes = { + getJob: PT.func.isRequired, + loading: PT.bool.isRequired, + job: PT.shape(), + id: PT.string.isRequired, +}; + +function mapStateToProps(state, ownProps) { + const data = state.recruitCRM[ownProps.id]; + return { + job: data ? data.job : {}, + loading: data ? data.loading : true, + }; +} + +function mapDispatchToActions(dispatch) { + const a = actions.recruit; + return { + getJob: (id) => { + dispatch(a.getJobInit(id)); + dispatch(a.getJobDone(id)); + }, + }; +} + +export default connect( + mapStateToProps, + mapDispatchToActions, +)(RecruitCRMJobDetailsContainer); diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx new file mode 100644 index 0000000000..1c37a6c510 --- /dev/null +++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx @@ -0,0 +1,153 @@ +/** + * 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 PT from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import './jobLisingStyles.scss'; + +const GIGS_PER_PAGE = 10; + +class RecruitCRMJobsContainer extends React.Component { + constructor(props) { + super(props); + // Filter initial state + this.state = { + term: '', + page: 0, + sortBy: 'created_on', + }; + + this.onSearch = this.onSearch.bind(this); + this.onPaginate = this.onPaginate.bind(this); + this.onFilter = this.onFilter.bind(this); + } + + componentDidMount() { + const { + getJobs, + jobs, + } = this.props; + + // This gets all jobs. + // Pagination and filtering on front-side + if (!jobs.length) { + getJobs({ + job_status: 1, // Open jobs only + }); + } + } + + /** + * 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, + }); + } + + onPaginate(newPage) { + this.onFilter({ + page: newPage.selected, + }); + } + + render() { + const { + loading, + jobs, + } = this.props; + const { + term, + page, + sortBy, + } = this.state; + + if (loading) { + return ; + } + + let jobsToDisplay = jobs; + // Filter by term + if (term) { + jobsToDisplay = _.filter(jobs, (job) => { + if (job.name.toLowerCase().includes(term.toLowerCase())) return true; + return false; + // add skills here + }); + } + // Sort controlled by sortBy state + jobsToDisplay = jobsToDisplay.sort((a, b) => 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.map(job => ) + } +
+ +
+ ); + } +} + +RecruitCRMJobsContainer.defaultProps = { + jobs: [], +}; + +RecruitCRMJobsContainer.propTypes = { + getJobs: PT.func.isRequired, + loading: PT.bool.isRequired, + jobs: PT.arrayOf(PT.shape), +}; + +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, +)(RecruitCRMJobsContainer); diff --git a/src/shared/containers/Gigs/jobLisingStyles.scss b/src/shared/containers/Gigs/jobLisingStyles.scss new file mode 100644 index 0000000000..ef403335ee --- /dev/null +++ b/src/shared/containers/Gigs/jobLisingStyles.scss @@ -0,0 +1,20 @@ +@import "~styles/mixins"; + +.container { + max-width: $screen-lg; + margin: auto; + + @include xs-to-sm { + padding: 0 15px; + } + + .filters { + display: flex; + } + + .jobs-list-container { + display: flex; + flex-direction: column; + margin: 20px 0 50px 0; + } +} diff --git a/src/shared/containers/GigsPages.jsx b/src/shared/containers/GigsPages.jsx new file mode 100644 index 0000000000..65b18b48a8 --- /dev/null +++ b/src/shared/containers/GigsPages.jsx @@ -0,0 +1,41 @@ +/** + * Connects the Redux store to the GigsPages component. + */ +import React from 'react'; +import PT from 'prop-types'; +import Header from 'containers/TopcoderHeader'; +import Footer from 'components/TopcoderFooter'; +import Viewport from 'components/Contentful/Viewport'; +import { config } from 'topcoder-react-utils'; +import RecruitCRMJobDetails from 'containers/Gigs/RecruitCRMJobDetails'; + + +export default function GigsPagesContainer(props) { + const { match } = props; + const { id } = match.params; + return ( +
+
+ { + id ? ( + + ) : ( + + ) + } +
+
+ ); +} + +GigsPagesContainer.defaultProps = { +}; + +GigsPagesContainer.propTypes = { + match: PT.shape().isRequired, +}; diff --git a/src/shared/reducers/recruitCRM.js b/src/shared/reducers/recruitCRM.js index c8fddd97f9..599080cc17 100644 --- a/src/shared/reducers/recruitCRM.js +++ b/src/shared/reducers/recruitCRM.js @@ -1,7 +1,7 @@ /** * Reducer for state.recruit */ - +import _ from 'lodash'; import actions from 'actions/recruitCRM'; import { handleActions } from 'redux-actions'; @@ -26,7 +26,35 @@ function onDone(state, { payload }) { return { ...state, loading: false, - jobs: payload.data, + jobs: _.filter(payload.data, job => job.enable_job_application_form === 1), + }; +} + +/** + * Handles recruit.getJobInit action. + * @param {Object} state Previous state. + */ +function onJobInit(state, { payload }) { + return { + ...state, + [payload.id]: { + loading: true, + }, + }; +} + +/** + * Handles recruit.getJobDone action. + * @param {Object} state Previous state. + * @param {Object} action The action. + */ +function onJobDone(state, { payload }) { + return { + ...state, + [payload.id]: { + loading: false, + job: payload.data, + }, }; } @@ -40,6 +68,8 @@ function create(state = {}) { return handleActions({ [actions.recruit.getJobsInit]: onInit, [actions.recruit.getJobsDone]: onDone, + [actions.recruit.getJobInit]: onJobInit, + [actions.recruit.getJobDone]: onJobDone, }, state); } diff --git a/src/shared/routes/GigsPages.jsx b/src/shared/routes/GigsPages.jsx new file mode 100644 index 0000000000..dca924c079 --- /dev/null +++ b/src/shared/routes/GigsPages.jsx @@ -0,0 +1,21 @@ +/** + * The loader of Gigs page webpack chunks. + */ +import React from 'react'; + +import LoadingPagePlaceholder from 'components/LoadingPagePlaceholder'; +import { AppChunk } from 'topcoder-react-utils'; + +export default function GigsPagesRoute(props) { + return ( + import(/* webpackChunkName: "gigsPages/chunk" */ 'containers/GigsPages') + .then(({ default: GigsPagesContainer }) => ( + + )) + } + renderPlaceholder={() => } + /> + ); +} diff --git a/src/shared/routes/index.jsx b/src/shared/routes/index.jsx index 04f1f6d25f..9c8eb25569 100644 --- a/src/shared/routes/index.jsx +++ b/src/shared/routes/index.jsx @@ -23,6 +23,7 @@ import Sandbox from './Sandbox'; import Topcoder from './Topcoder'; import TrackHomePages from './TrackHomePages'; import PolicyPages from './PolicyPages'; +import GigsPages from './GigsPages'; function Routes({ communityId }) { const metaTags = ( @@ -97,6 +98,11 @@ function Routes({ communityId }) { exact path={`${config.POLICY_PAGES_PATH}/:slug?`} /> + diff --git a/src/shared/services/recruitCRM.js b/src/shared/services/recruitCRM.js index 43b497d923..b3b1c279cc 100644 --- a/src/shared/services/recruitCRM.js +++ b/src/shared/services/recruitCRM.js @@ -35,4 +35,17 @@ export default class Service { } return res.json(); } + + /** + * Get all jobs + * @param {*} query The request query + */ + async getAllJobs(query) { + const res = await fetch(`${this.baseUrl}/jobs?${qs.stringify(query)}`); + if (!res.ok) { + const error = new Error('Failed to get jobs'); + logger.error(error, res); + } + return res.json(); + } } diff --git a/src/shared/utils/gigs.js b/src/shared/utils/gigs.js new file mode 100644 index 0000000000..63d42a1201 --- /dev/null +++ b/src/shared/utils/gigs.js @@ -0,0 +1,28 @@ +/** + * Gig work utils + */ + +import _ from 'lodash'; + +/** + * Salary Type mapper + * @param {Object} data the data + */ +export function getSalaryType(data) { + switch (data.id) { + case 2: return 'annual'; + case 3: return 'week'; + default: return 'n/a'; + } +} + +/** + * Custom Field mapper + * @param {Array} data the data + */ +export function getCustomField(data, key) { + const val = _.find(data, { + field_name: key, + }); + return val && val.value ? val.value : 'n/a'; +} From 16be1cb77a0549fa74c64d610b8108697f174de1 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Tue, 25 Aug 2020 16:39:15 +0300 Subject: [PATCH 06/16] Env support for gig work APIs --- Dockerfile | 1 + build.sh | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 4defb8130c..5c64e31015 100644 --- a/Dockerfile +++ b/Dockerfile @@ -115,6 +115,7 @@ ENV CONTENTFUL_MANAGEMENT_TOKEN=$CONTENTFUL_MANAGEMENT_TOKEN ENV CONTENTFUL_EDU_SPACE_ID=$CONTENTFUL_EDU_SPACE_ID ENV CONTENTFUL_EDU_CDN_API_KEY=$CONTENTFUL_EDU_CDN_API_KEY ENV CONTENTFUL_EDU_PREVIEW_API_KEY=$CONTENTFUL_EDU_PREVIEW_API_KEY +ENV RECRUITCRM_API_KEY=$RECRUITCRM_API_KEY ################################################################################ # Testing and build of the application inside the container. diff --git a/build.sh b/build.sh index bb62f0c990..64affb8170 100755 --- a/build.sh +++ b/build.sh @@ -42,7 +42,8 @@ docker build -t $TAG \ --build-arg TC_M2M_GRANT_TYPE=$TC_M2M_GRANT_TYPE \ --build-arg CONTENTFUL_COMCAST_SPACE_ID=$CONTENTFUL_COMCAST_SPACE_ID \ --build-arg CONTENTFUL_COMCAST_CDN_API_KEY=$CONTENTFUL_COMCAST_CDN_API_KEY \ - --build-arg CONTENTFUL_COMCAST_PREVIEW_API_KEY=$CONTENTFUL_COMCAST_PREVIEW_API_KEY . + --build-arg CONTENTFUL_COMCAST_PREVIEW_API_KEY=$CONTENTFUL_COMCAST_PREVIEW_API_KEY \ + --build-arg RECRUITCRM_API_KEY=$RECRUITCRM_API_KEY . # Copies "node_modules" from the created image, if necessary for caching. docker create --name app $TAG From ed8b31bc07d764e7f26feaced87a6d3441536b90 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Tue, 25 Aug 2020 22:57:07 +0300 Subject: [PATCH 07/16] Error handling on recruitAPI calls --- src/server/services/recruitCRM.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js index 20f3e35b63..9213f221df 100644 --- a/src/server/services/recruitCRM.js +++ b/src/server/services/recruitCRM.js @@ -44,6 +44,7 @@ export default class RecruitCRMService { return res.send({ error: true, status: response.status, + response, }); } const data = await response.json(); @@ -75,6 +76,7 @@ export default class RecruitCRMService { return res.send({ error: true, status: response.status, + response, }); } const data = await response.json(); @@ -106,6 +108,7 @@ export default class RecruitCRMService { return res.send({ error: true, status: response.status, + response, }); } const data = await response.json(); From eac7d775cc74d3716b48a6b581c1c62c6afecbe0 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Tue, 25 Aug 2020 23:04:04 +0300 Subject: [PATCH 08/16] url in error rsp --- src/server/services/recruitCRM.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js index 9213f221df..875e182ad3 100644 --- a/src/server/services/recruitCRM.js +++ b/src/server/services/recruitCRM.js @@ -45,6 +45,7 @@ export default class RecruitCRMService { error: true, status: response.status, response, + url: `${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}`, }); } const data = await response.json(); @@ -77,6 +78,7 @@ export default class RecruitCRMService { error: true, status: response.status, response, + url: `${this.private.baseUrl}/v1/jobs/${req.params.id}`, }); } const data = await response.json(); @@ -109,6 +111,7 @@ export default class RecruitCRMService { error: true, status: response.status, response, + url: `${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}`, }); } const data = await response.json(); From 32c155f875c2d09b4da63d923dd89b6e6f133dc8 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Tue, 25 Aug 2020 23:26:31 +0300 Subject: [PATCH 09/16] relative API url --- src/server/services/recruitCRM.js | 6 +++--- src/shared/services/recruitCRM.js | 5 +---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js index 875e182ad3..07215a07f9 100644 --- a/src/server/services/recruitCRM.js +++ b/src/server/services/recruitCRM.js @@ -44,8 +44,8 @@ export default class RecruitCRMService { return res.send({ error: true, status: response.status, - response, url: `${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}`, + key: this.private.authorization, }); } const data = await response.json(); @@ -77,8 +77,8 @@ export default class RecruitCRMService { return res.send({ error: true, status: response.status, - response, url: `${this.private.baseUrl}/v1/jobs/${req.params.id}`, + key: this.private.authorization, }); } const data = await response.json(); @@ -110,8 +110,8 @@ export default class RecruitCRMService { return res.send({ error: true, status: response.status, - response, url: `${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}`, + key: this.private.authorization, }); } const data = await response.json(); diff --git a/src/shared/services/recruitCRM.js b/src/shared/services/recruitCRM.js index b3b1c279cc..0c124ef6ee 100644 --- a/src/shared/services/recruitCRM.js +++ b/src/shared/services/recruitCRM.js @@ -1,11 +1,8 @@ -import { config } from 'topcoder-react-utils'; import fetch from 'isomorphic-fetch'; import { logger } from 'topcoder-react-lib'; import qs from 'qs'; -const LOCAL_MODE = Boolean(config.CONTENTFUL.LOCAL_MODE); - -const PROXY_ENDPOINT = `${LOCAL_MODE ? '' : config.URL.APP}/api/recruit`; +const PROXY_ENDPOINT = '/api/recruit'; export default class Service { baseUrl = PROXY_ENDPOINT; From e0e825737bdaffed060631297a9beb6174a752b0 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 26 Aug 2020 00:21:45 +0300 Subject: [PATCH 10/16] Docker file for env update --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 5c64e31015..49e686a794 100644 --- a/Dockerfile +++ b/Dockerfile @@ -43,6 +43,7 @@ ARG CONTENTFUL_EDU_PREVIEW_API_KEY ARG FILESTACK_API_KEY ARG FILESTACK_SUBMISSION_CONTAINER +ARG RECRUITCRM_API_KEY # Credentials for Mailchimp service ARG MAILCHIMP_API_KEY From f3c76dc33db5239df5483cc05a7dd11cda072ab4 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Wed, 26 Aug 2020 13:07:17 +0530 Subject: [PATCH 11/16] ci: deploy on staging --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 76c90ab694..1c6c720f9d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -258,7 +258,7 @@ workflows: filters: branches: only: - - hot-fix + - feature-contentful # Production builds are exectuted # when PR is merged to the master # Don't change anything in this configuration From 982783dd25647981f964b324e95241bb498010db Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 26 Aug 2020 11:21:09 +0300 Subject: [PATCH 12/16] Team fixes 1 for #4788 --- src/server/services/recruitCRM.js | 3 --- .../components/GUIKit/JobListCard/index.jsx | 2 +- .../components/GUIKit/JobListCard/style.scss | 11 ++++++++++- src/shared/containers/Gigs/RecruitCRMJobs.jsx | 16 ++++++++++++---- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js index 07215a07f9..4f7fb95189 100644 --- a/src/server/services/recruitCRM.js +++ b/src/server/services/recruitCRM.js @@ -45,7 +45,6 @@ export default class RecruitCRMService { error: true, status: response.status, url: `${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}`, - key: this.private.authorization, }); } const data = await response.json(); @@ -78,7 +77,6 @@ export default class RecruitCRMService { error: true, status: response.status, url: `${this.private.baseUrl}/v1/jobs/${req.params.id}`, - key: this.private.authorization, }); } const data = await response.json(); @@ -111,7 +109,6 @@ export default class RecruitCRMService { error: true, status: response.status, url: `${this.private.baseUrl}/v1/jobs/search?${qs.stringify(req.query)}`, - key: this.private.authorization, }); } const data = await response.json(); diff --git a/src/shared/components/GUIKit/JobListCard/index.jsx b/src/shared/components/GUIKit/JobListCard/index.jsx index 52316f8091..b95f055fa4 100644 --- a/src/shared/components/GUIKit/JobListCard/index.jsx +++ b/src/shared/components/GUIKit/JobListCard/index.jsx @@ -28,7 +28,7 @@ export default function JobListCard({ const hoursPerWeek = getCustomField(job.custom_fields, 'Hours per week'); return (
-
{job.name}
+ {job.name}
{skills} diff --git a/src/shared/components/GUIKit/JobListCard/style.scss b/src/shared/components/GUIKit/JobListCard/style.scss index 63ebb264ea..f71be805bd 100644 --- a/src/shared/components/GUIKit/JobListCard/style.scss +++ b/src/shared/components/GUIKit/JobListCard/style.scss @@ -14,10 +14,19 @@ @include gui-kit-content; @include roboto-regular; - h6 { + .gig-name, + .gig-name:visited, + .gig-name:active, + .gig-name:hover { color: #1e94a3; margin-top: 0; margin-bottom: 12px; + text-decoration: none; + font-family: Barlow, sans-serif; + font-size: 20px; + font-weight: 600; + line-height: 24px; + text-transform: uppercase; @include xs-to-sm { margin-bottom: 20px; diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx index 1c37a6c510..e1d956a119 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobs.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx @@ -22,7 +22,7 @@ class RecruitCRMJobsContainer extends React.Component { this.state = { term: '', page: 0, - sortBy: 'created_on', + sortBy: 'updated_on', }; this.onSearch = this.onSearch.bind(this); @@ -88,9 +88,17 @@ class RecruitCRMJobsContainer extends React.Component { // Filter by term if (term) { jobsToDisplay = _.filter(jobs, (job) => { - if (job.name.toLowerCase().includes(term.toLowerCase())) return true; + // 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; + // no match return false; - // add skills here }); } // Sort controlled by sortBy state @@ -106,7 +114,7 @@ class RecruitCRMJobsContainer extends React.Component { return (
- +
{ From 1b9f6de4744285ec36ce0348a47f4727ee5cad98 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 26 Aug 2020 13:49:36 +0300 Subject: [PATCH 13/16] Team fixes 2 --- .../components/GUIKit/JobListCard/style.scss | 6 +++--- .../components/GUIKit/SearchCombo/index.jsx | 5 ++++- .../components/GUIKit/SearchCombo/style.scss | 21 ++++++++++++------- src/shared/components/Gigs/GigDetails.jsx | 4 ++++ src/shared/components/Gigs/style.scss | 13 +++++++++++- src/shared/containers/Gigs/RecruitCRMJobs.jsx | 4 ++-- 6 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/shared/components/GUIKit/JobListCard/style.scss b/src/shared/components/GUIKit/JobListCard/style.scss index f71be805bd..c4c46cb44e 100644 --- a/src/shared/components/GUIKit/JobListCard/style.scss +++ b/src/shared/components/GUIKit/JobListCard/style.scss @@ -53,15 +53,15 @@ } &:nth-child(2) { - width: 204px; + width: 174px; } &:nth-child(3) { - width: 263px; + width: 233px; } &:nth-child(4) { - width: 195px; + width: 225px; } &:last-child { diff --git a/src/shared/components/GUIKit/SearchCombo/index.jsx b/src/shared/components/GUIKit/SearchCombo/index.jsx index 2ec937621e..29400e9cb0 100644 --- a/src/shared/components/GUIKit/SearchCombo/index.jsx +++ b/src/shared/components/GUIKit/SearchCombo/index.jsx @@ -21,7 +21,10 @@ function SearchCombo({ return (
- setVal(event.target.value)} /> + { + !inputVal ? {placeholder} : null + } + setVal(event.target.value)} /> { inputVal ? : null } diff --git a/src/shared/components/GUIKit/SearchCombo/style.scss b/src/shared/components/GUIKit/SearchCombo/style.scss index 4f25854885..a47b4940ec 100644 --- a/src/shared/components/GUIKit/SearchCombo/style.scss +++ b/src/shared/components/GUIKit/SearchCombo/style.scss @@ -9,26 +9,33 @@ width: 100%; position: relative; margin-right: 10px; + z-index: 1; input.input { - background-color: #fff; + background: transparent; border: 1px solid #aaa; border-radius: 6px; height: 39px; margin: 0; + } - &::placeholder { - color: #aaa; - font-size: 14px; - line-height: 22px; - text-transform: none; - } + .search-placeholder { + color: #aaa; + font-size: 14px; + font-family: Roboto, sans-serif; + line-height: 22px; + text-transform: none; + position: absolute; + z-index: 0; + top: 8px; + left: 15px; } .clear-search { position: absolute; top: calc(50% - 5px); right: 15px; + cursor: pointer; } } diff --git a/src/shared/components/Gigs/GigDetails.jsx b/src/shared/components/Gigs/GigDetails.jsx index faa587e607..cbf3602331 100644 --- a/src/shared/components/Gigs/GigDetails.jsx +++ b/src/shared/components/Gigs/GigDetails.jsx @@ -35,6 +35,8 @@ export default function GigDetails(props) { if (isomorphy.isClientSide()) { shareUrl = encodeURIComponent(window.location.href); } + let skills = getCustomField(job.custom_fields, 'Technologies Required'); + if (skills !== 'n/a') skills = skills.split(',').join(', '); return (
@@ -88,6 +90,8 @@ export default function GigDetails(props) {
+

Required Tech Skills

+

{skills}

Description

{ReactHtmlParser(job.job_description_text, ReactHtmlParserOptions)}

diff --git a/src/shared/components/Gigs/style.scss b/src/shared/components/Gigs/style.scss index 5c4ea6b989..03756963e1 100644 --- a/src/shared/components/Gigs/style.scss +++ b/src/shared/components/Gigs/style.scss @@ -134,6 +134,10 @@ } } } + + .gig-skills { + display: flex; + } } .left { @@ -146,8 +150,15 @@ line-height: 30px; display: inline-block; } - /* stylelint-enable */ + + h4 { + margin-top: 35px; + + &:first-child { + margin-top: 0; + } + } } } } diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx index e1d956a119..eacff10bde 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobs.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx @@ -114,11 +114,11 @@ class RecruitCRMJobsContainer extends React.Component { return (
- +
{ - jobsToDisplay.map(job => ) + jobsToDisplay.map(job => ) }
From 0f553b52f66f6bd6a4bfeb8a0ca26c8672d5f689 Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Wed, 26 Aug 2020 17:22:51 +0300 Subject: [PATCH 14/16] Remove h/week from listing --- src/shared/components/GUIKit/JobListCard/index.jsx | 6 +----- src/shared/components/GUIKit/JobListCard/style.scss | 6 +++--- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/shared/components/GUIKit/JobListCard/index.jsx b/src/shared/components/GUIKit/JobListCard/index.jsx index b95f055fa4..d94f4dc175 100644 --- a/src/shared/components/GUIKit/JobListCard/index.jsx +++ b/src/shared/components/GUIKit/JobListCard/index.jsx @@ -7,7 +7,6 @@ import PT from 'prop-types'; import { config, Link } from 'topcoder-react-utils'; import { getSalaryType, getCustomField } from 'utils/gigs'; import './style.scss'; -import IconBlackCalendar from 'assets/images/icon-black-calendar.svg'; import IconBlackDuration from 'assets/images/icon-black-duration.svg'; import IconBlackLocation from 'assets/images/icon-black-location.svg'; import IconBlackPayment from 'assets/images/icon-black-payment.svg'; @@ -25,7 +24,7 @@ export default function JobListCard({ skills = skills.join(', '); } } - const hoursPerWeek = getCustomField(job.custom_fields, 'Hours per week'); + return (
{job.name} @@ -42,9 +41,6 @@ export default function JobListCard({
{getCustomField(job.custom_fields, 'Duration')}
-
- {`${hoursPerWeek !== 'n/a' ? `${hoursPerWeek} hours / week` : 'n/a'}`} -
VIEW DETAILS
diff --git a/src/shared/components/GUIKit/JobListCard/style.scss b/src/shared/components/GUIKit/JobListCard/style.scss index c4c46cb44e..1cff03c199 100644 --- a/src/shared/components/GUIKit/JobListCard/style.scss +++ b/src/shared/components/GUIKit/JobListCard/style.scss @@ -53,15 +53,15 @@ } &:nth-child(2) { - width: 174px; + width: 204px; } &:nth-child(3) { - width: 233px; + width: 263px; } &:nth-child(4) { - width: 225px; + width: 255px; } &:last-child { From 630726a2ad3ae5baa65dcbf5e6052239e510e4d7 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Wed, 26 Aug 2020 22:35:27 +0530 Subject: [PATCH 15/16] ci: deploy on dev --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1c6c720f9d..e736f24eb1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -230,7 +230,7 @@ workflows: filters: branches: only: - - develop + - feature-contentful # This is alternate dev env for parallel testing - "build-test": context : org-global From 7d332fb7a861a251483884713f4341c98546ec36 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Thu, 27 Aug 2020 10:37:53 +0530 Subject: [PATCH 16/16] fix: for gigs work on prod --- config/production.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/production.js b/config/production.js index 35adcd73a3..d62be0f17d 100644 --- a/config/production.js +++ b/config/production.js @@ -115,7 +115,7 @@ module.exports = { }, { title: 'Gig Work', - href: '/community/taas', + href: '/gigs', }, { title: 'Practice',