From 83a6d017fc0188f6cdbaa9d1581c8e5c0a348bd0 Mon Sep 17 00:00:00 2001 From: M Fikri A Date: Fri, 19 Mar 2021 09:29:33 +0700 Subject: [PATCH 01/16] Implement Secure Identity Verification --- config/custom-environment-variables.js | 1 + docs/secure-identity-verification.md | 9 +++++++++ src/client/index.jsx | 25 ++++++++++++++++++++----- src/shared/reducers/index.js | 24 +++++++++++++++++++++++- 4 files changed, 53 insertions(+), 6 deletions(-) create mode 100644 docs/secure-identity-verification.md diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index 34ffc3cb89..280e961b4f 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -99,6 +99,7 @@ module.exports = { RECRUITCRM_API_KEY: 'RECRUITCRM_API_KEY', GROWSURF_API_KEY: 'GROWSURF_API_KEY', SENDGRID_API_KEY: 'SENDGRID_API_KEY', + CHAMELEON_VERIFICATION_SECRET: 'CHAMELEON_VERIFICATION_SECRET', }, GROWSURF_CAMPAIGN_ID: 'GROWSURF_CAMPAIGN_ID', AUTH_CONFIG: { diff --git a/docs/secure-identity-verification.md b/docs/secure-identity-verification.md new file mode 100644 index 0000000000..4f5d6bb2a5 --- /dev/null +++ b/docs/secure-identity-verification.md @@ -0,0 +1,9 @@ +## Setup +1. Make sure you have a Chameleon Account and segment.com account +2. Integrate Chameleon Account with Segment. https://help.trychameleon.com/en/articles/1161770-installing-using-segment +3. Set Environment secret variable retrieved here https://app.trychameleon.com/settings/integrations/segment. Run the following command +`export CHAMELEON_VERIFICATION_SECRET=` +4. Run community app + +## Verification +1. When you reload the page you will notice in the network tab there will be 2 requests POST to https://api.segment.io/v1/i, one will send it to segment and one will send only to chameleon (with request payload `{ integrations: { All: false, Chameleon: true }}`) diff --git a/src/client/index.jsx b/src/client/index.jsx index b44745e76e..b879711ac2 100644 --- a/src/client/index.jsx +++ b/src/client/index.jsx @@ -23,9 +23,10 @@ const { setErrorsStore } = errors; * Performs AnalyticsJS identification of the user. * @param {Object} profile TC user profile. * @param {Array} roles User roles. + * @param {String} userIdHash Unique Hash per user. */ -function identify(profile, roles) { - analytics.identify(profile.userId, { +function identify(profile, roles, userIdHash) { + const payload = { avatar: profile.photoURL, createdAt: profile.createdAt, email: profile.email, @@ -39,7 +40,21 @@ function identify(profile, roles) { })), tracks: profile.tracks || [], username: profile.handle, - }); + }; + analytics.identify( + profile.userId, + payload, + { + integrations: { Chameleon: false }, + }, + ); + analytics.identify( + profile.userId, + { uid_hash: userIdHash, ...payload }, + { + integrations: { All: false, Chameleon: true }, + }, + ); } /** @@ -74,7 +89,7 @@ function authenticate(store) { }).then(({ tctV2, tctV3 }) => { const { auth } = store.getState(); if (auth.profile && !analyticsIdentitySet) { - identify(auth.profile, _.get(auth, 'user.roles')); + identify(auth.profile, _.get(auth, 'user.roles'), auth.userIdHash); analyticsIdentitySet = true; } if (auth.tokenV3 !== (tctV3 || null)) { @@ -85,7 +100,7 @@ function authenticate(store) { const userId = profile && profile.userId; const prevUserId = _.get(store.getState(), 'auth.profile.userId'); if (userId && userId !== prevUserId) { - identify(profile, _.get(auth, user.roles)); + identify(profile, _.get(auth, user.roles), auth.userIdHash); analyticsIdentitySet = true; } }); diff --git a/src/shared/reducers/index.js b/src/shared/reducers/index.js index 518f7de831..2a49d02df0 100644 --- a/src/shared/reducers/index.js +++ b/src/shared/reducers/index.js @@ -15,8 +15,9 @@ */ import _ from 'lodash'; +import crypto from 'crypto'; import { getCommunityId } from 'server/services/communities'; -import { redux } from 'topcoder-react-utils'; +import { redux, config, isomorphy } from 'topcoder-react-utils'; import { reducer as toastrReducer } from 'react-redux-toastr'; import { reducerFactory } from 'topcoder-react-lib'; import { getAuthTokens } from 'utils/tc'; @@ -113,6 +114,21 @@ function generateSsrOptions(req) { return res; } +/** + * Generate user id hash for secure Identity verification + * @param {Object} user + * @return {String} User Id Hash. + */ +function generateUserIdHash(user) { + const secret = _.get(config, 'SECRET.CHAMELEON_VERIFICATION_SECRET'); + const now = Math.floor(Date.now() / 1000); + + return [ + crypto.createHmac('sha256', secret).update(`${user.id}-${now}`).digest('hex'), + now, + ].join('-'); +} + export function factory(req) { return redux.resolveReducers({ standard: reducerFactory(req && generateSsrOptions(req)), @@ -125,6 +141,12 @@ export function factory(req) { page: pageFactory(req), }).then(resolvedReducers => redux.combineReducers((state) => { const res = { ...state }; + + const user = _.get(res, 'auth.user'); + if (user && isomorphy.isServerSide()) { + res.auth.userIdHash = generateUserIdHash(user); + } + if (req) { res.domain = `${req.protocol}://${req.headers.host || req.hostname}`; res.subdomainCommunity = getCommunityId(req.subdomains); From c416d4e77b8ee708edc16ead6e1458ad472cf329 Mon Sep 17 00:00:00 2001 From: M Fikri A Date: Fri, 19 Mar 2021 10:05:49 +0700 Subject: [PATCH 02/16] Use userId --- docs/secure-identity-verification.md | 6 +++++- src/shared/reducers/index.js | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/secure-identity-verification.md b/docs/secure-identity-verification.md index 4f5d6bb2a5..896bc450ad 100644 --- a/docs/secure-identity-verification.md +++ b/docs/secure-identity-verification.md @@ -6,4 +6,8 @@ 4. Run community app ## Verification -1. When you reload the page you will notice in the network tab there will be 2 requests POST to https://api.segment.io/v1/i, one will send it to segment and one will send only to chameleon (with request payload `{ integrations: { All: false, Chameleon: true }}`) +1. Log in to topcoder-dev account +2. Access http://local.topcoder-dev.com/challenges +3. You will notice in the network tab there will be 2 requests POST to https://api.segment.io/v1/i, one will send it to segment and one will send only to chameleon (with request payload `{ integrations: { All: false, Chameleon: true }}`) + +Repeat the proses and log in to different account and make sure the `uid_hash` is different for each different user. diff --git a/src/shared/reducers/index.js b/src/shared/reducers/index.js index 2a49d02df0..42fcb4d19c 100644 --- a/src/shared/reducers/index.js +++ b/src/shared/reducers/index.js @@ -124,7 +124,7 @@ function generateUserIdHash(user) { const now = Math.floor(Date.now() / 1000); return [ - crypto.createHmac('sha256', secret).update(`${user.id}-${now}`).digest('hex'), + crypto.createHmac('sha256', secret).update(`${user.userId}-${now}`).digest('hex'), now, ].join('-'); } From 38931560832f7fd4be377ab104688ccf7812444d Mon Sep 17 00:00:00 2001 From: Luiz Ricardo Rodrigues Date: Fri, 2 Apr 2021 04:52:01 -0300 Subject: [PATCH 03/16] ci: Deploy update-segment-script to Dev and Stag --- .circleci/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7d107a6dc6..a67f61b412 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -276,7 +276,7 @@ workflows: branches: only: - develop - - feature/recommender-sync-develop + - update-segment-script # This is alternate dev env for parallel testing - "build-test": context : org-global @@ -305,7 +305,7 @@ workflows: branches: only: - develop - - feature/recommender-sync-develop + - update-segment-script - "approve-smoke-test-on-staging": type: approval requires: From 0922cada0bd34a3f5f206fc5bb954e74964df8ca Mon Sep 17 00:00:00 2001 From: Kiril Kartunov Date: Mon, 12 Apr 2021 11:15:34 +0300 Subject: [PATCH 04/16] Implement v1 #5479 --- config/default.js | 3 + package.json | 1 + src/assets/images/gig-work/tag-dolars.png | Bin 0 -> 1993 bytes src/assets/images/gig-work/tag-hot.png | Bin 0 -> 1556 bytes src/assets/images/gig-work/tag-new.png | Bin 0 -> 1928 bytes .../components/GUIKit/JobListCard/index.jsx | 12 ++ .../components/GUIKit/JobListCard/style.scss | 32 +++- src/shared/containers/Gigs/RecruitCRMJobs.jsx | 74 ++++++-- .../containers/Gigs/jobLisingStyles.scss | 164 +++++++++++++++++- src/shared/containers/GigsPages.jsx | 23 ++- src/shared/routes/index.jsx | 25 ++- 11 files changed, 304 insertions(+), 30 deletions(-) create mode 100644 src/assets/images/gig-work/tag-dolars.png create mode 100644 src/assets/images/gig-work/tag-hot.png create mode 100644 src/assets/images/gig-work/tag-new.png diff --git a/config/default.js b/config/default.js index 161136e9e2..ec258f40b2 100644 --- a/config/default.js +++ b/config/default.js @@ -426,4 +426,7 @@ module.exports = { DEBOUNCE_ON_CHANGE_TIME: 150, }, ENABLE_RECOMMENDER: true, + OPTIMIZELY: { + SDK_KEY: '7V4CJhurXT3Y3bnzv1hv1', + }, }; diff --git a/package.json b/package.json index 28e53ff3ad..4c43b84f24 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ }, "dependencies": { "@hapi/joi": "^16.1.4", + "@optimizely/react-sdk": "^2.5.0", "@topcoder-platform/tc-auth-lib": "topcoder-platform/tc-auth-lib#1.0.4", "aos": "^2.3.4", "atob": "^2.1.1", diff --git a/src/assets/images/gig-work/tag-dolars.png b/src/assets/images/gig-work/tag-dolars.png new file mode 100644 index 0000000000000000000000000000000000000000..ec9ca0030141a9bbd2a2326578215696cce8f8a5 GIT binary patch literal 1993 zcmV;)2R8VLP)P001}$1^@s62pLoG00001b5ch_0Itp) z=>Px+gGod|R9JFGOPaV6ZVp10jHk?**-d++n?nwWT-={z%Y?>+bY|NrxHR{*w-4JitsfbidjDu^V{5dTxZMQJ>};x?%Q z;P4X!kvSH4+k&8~;j^m$EuAd_3h542I~BIMH(=o)qu6h$=sKA)c-KnupA5kQo% z0SM}n(|KcMLpfFwrle4-i_3Gkw75uBb5HC9YKtX-N|=j73CQH|7U0U=wTTjlHN+zR zc2qzzJOPQPrRU%Y5lUNQ`v5poQo=|6u7v|eVWg0nCx%~wKv-Y~gFAvl{nXsFbLx4P zU^G2=@Fd{vU2q96s+tQUIdMKs{d0uZo>OH;2`6BS{}&;YoO>5O`UB}%hGRFo{?D5HXiSkba)5GPL7(O8`>;N&U3HW-2 z1^Xrx35eM6MsgksCrnZeA~DGPXA_3Hq5v>%%ETM7;UsnnJCWuKO|~HgCBi5NV5-)T zyYgM4&Wj5O@Pzs1bI1r9AShjo2!J~AcNj1hu_+(ikB+?=$nf(??%U`OR z{*%`0e+v*bUO7+m_@B{?_pw9Drt%wym3x1@oyt@f4LSyQtlEGpb7U z+H05@`aSOA-|5`7!&kmyMNQx;DoPNly{oDAu3>!e8%#a$Q?jMq%>3>UojX5+wj7O9 zr)j)+iuUavqwUtsLBcUfYJfb#XWBOo+S zj?g$cnxC}kXg+_8#?ceBUccb;Ub?ote$IMgtmHbM{?RXJ9zH_b*3DE`_b~O)PiP%| z38~d+JUK+?JzqnXcKh5{l)rJD1%xcu_&{vE@g|K!M`%9#M>+-vsD18sT4U#F4Zldo zorBD~@7u`I_b{{X0Pg$+AMp~nFkXTuq0dUPuKV`{ZtOKsC4293RF%g5gH%?oAbbBx z5FuN>40qD}K)7B8lC`Yat^x&s<5 zW)3_<>xJXgc6^%3O*c~5r__@rxeL-~%UiXOn6}(tLamm5uAk zHmr}(Byi{JW4>nXk!Q3RjHEf%)24x3QP7ryYQ@rTp+u)@J>GY-BsD%xv>r=Qn4YHj z^kHP-0dLq;9ef1x~-o~h5G#u zqHkR!yZl|uJp4my3m1}Ivl0MjhOCUvCf-QPpIYa}aKp!Nr_a)M_t&W`TZS7MMPGf5 z+C6uZb#*cGz58hH9ilopg$day{ zu|Y^sEX29aLJrX~ken8g1VORR#u-=l%c_<5br6!d1)A@p1IR8&p2^$=m?3xihFF zsV4$({ozB_-bKbj{fVVH6{xjkY5SVyE>qcwEn6hQWgev)sr9>wT9T$ulh0{~6Rw2C zHZ^z_yU6k_*e*T@&Y-nxV7On#Bc`zuFQ%bzn*21G?EOr8DlNgA4lUkqN^ofcHr^BK zD)V52v_q8H1=P zuA*A3b5EcP7ZiU<4euT<<0paGM)GHm-(yewpFA3n;sGooqOBahj=;!mp~etmpqRkdD#*4bRT6)vgnj bM*#iP001}$1^@s62pLoG00001b5ch_0Itp) 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 literal 0 HcmV?d00001 diff --git a/src/shared/components/GUIKit/JobListCard/index.jsx b/src/shared/components/GUIKit/JobListCard/index.jsx index 6502ff43bd..ac5052f261 100644 --- a/src/shared/components/GUIKit/JobListCard/index.jsx +++ b/src/shared/components/GUIKit/JobListCard/index.jsx @@ -11,7 +11,15 @@ 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.png'; +import newTag from 'assets/images/gig-work/tag-new.png'; +import hotTag from 'assets/images/gig-work/tag-hot.png'; +import dolarsTag from 'assets/images/gig-work/tag-dolars.png'; +const TAGS = { + New: newTag, + Hot: hotTag, + $$$: dolarsTag, +}; export default function JobListCard({ job, }) { @@ -25,9 +33,13 @@ export default function JobListCard({ skills = skills.join(', '); } } + const tag = getCustomField(job.custom_fields, 'Job Tag'); return (
+ { + tag !== 'n/a' && gig-job-tag + } {job.name}
diff --git a/src/shared/components/GUIKit/JobListCard/style.scss b/src/shared/components/GUIKit/JobListCard/style.scss index 1252ab2fd4..63f8551b5c 100644 --- a/src/shared/components/GUIKit/JobListCard/style.scss +++ b/src/shared/components/GUIKit/JobListCard/style.scss @@ -7,13 +7,23 @@ display: flex; flex-direction: column; color: #2a2a2a; - padding: 25px 35px; + padding: 25px 35px 25px 44px; margin-bottom: 15px; + position: relative; @include gui-kit-headers; @include gui-kit-content; @include roboto-regular; + .gig-tag { + position: absolute; + top: 0; + left: 10px; + width: 24px; + height: 56px; + border-radius: 0; + } + .gig-name, .gig-name:visited, .gig-name:active, @@ -50,18 +60,38 @@ &:first-child { width: 250px; + + @media (max-width: 1280px) { + width: auto; + margin-right: 20px; + } } &:nth-child(2) { width: 204px; + + @media (max-width: 1280px) { + width: auto; + margin-right: 20px; + } } &:nth-child(3) { width: 263px; + + @media (max-width: 1280px) { + width: auto; + margin-right: 20px; + } } &:nth-child(4) { width: 255px; + + @media (max-width: 1280px) { + width: auto; + margin-right: 20px; + } } &:last-child { diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx index eb649611ad..cb7340d45a 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobs.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx @@ -12,8 +12,12 @@ 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 } from 'topcoder-react-utils'; import './jobLisingStyles.scss'; +const CONTENT_PREVIEW_LENGTH = 175; const GIGS_PER_PAGE = 10; // Sort by dropdown const sortByOptions = [ @@ -120,6 +124,13 @@ class RecruitCRMJobsContainer extends React.Component { } 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) => { @@ -160,7 +171,14 @@ class RecruitCRMJobsContainer extends React.Component { }); } // Sort controlled by sortBy state - jobsToDisplay = jobsToDisplay.sort((a, b) => new Date(b[sortBy]) - new Date(a[sortBy])); + jobsToDisplay = jobsToDisplay.sort((a, b) => { + // sort tags first no matter the sortBy + const tagA = getCustomField(a.custom_fields, 'Job Tag'); + const tagB = getCustomField(b.custom_fields, 'Job Tag'); + if (tagB !== 'n/a' && tagA === 'n/a') return Number.MAX_VALUE; + if (tagB === 'n/a' && tagA !== '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 @@ -170,22 +188,54 @@ class RecruitCRMJobsContainer extends React.Component { ); return ( -
-
- - - -
-
+
+
+
+ + + +
+
+ { + jobsToDisplay.length + ? jobsToDisplay.map(job => ) + : No Results + } +
{ jobsToDisplay.length - ? jobsToDisplay.map(job => ) - : No Results + ? : null }
{ - jobsToDisplay.length - ? : null + hotlistJobs.length && ( +
+
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)}
+ + ) : ( +
+
{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)}...`}` + } +
+ Apply Now +
+ ))) + } +
+
+ ) }
); diff --git a/src/shared/containers/Gigs/jobLisingStyles.scss b/src/shared/containers/Gigs/jobLisingStyles.scss index 923c4fe8fb..af7cc32555 100644 --- a/src/shared/containers/Gigs/jobLisingStyles.scss +++ b/src/shared/containers/Gigs/jobLisingStyles.scss @@ -1,3 +1,4 @@ +/* stylelint-disable no-descending-specificity */ @import "~styles/mixins"; .loading-text { @@ -8,14 +9,19 @@ text-align: center; } -.container { +.container, +.container-with-hotlist { max-width: $screen-lg; margin: auto; - @include xs-to-sm { + @media (max-width: 1280px) { padding: 0 15px; } + .gigs { + display: block; + } + .filters { display: flex; align-items: flex-end; @@ -30,6 +36,7 @@ @include xs-to-sm { margin-right: 0; + margin-bottom: 15px; } &:first-child { @@ -53,3 +60,156 @@ } } } + +.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: 22px; + } + } + + .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/GigsPages.jsx b/src/shared/containers/GigsPages.jsx index 9945ba4962..78565dc054 100644 --- a/src/shared/containers/GigsPages.jsx +++ b/src/shared/containers/GigsPages.jsx @@ -6,19 +6,22 @@ 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 { config, isomorphy } from 'topcoder-react-utils'; import RecruitCRMJobDetails from 'containers/Gigs/RecruitCRMJobDetails'; import { Helmet } from 'react-helmet'; import MetaTags from 'components/MetaTags'; +import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk'; - +const optimizelyClient = createInstance({ + sdkKey: config.OPTIMIZELY.SDK_KEY, +}); export default function GigsPagesContainer(props) { const { match } = props; - const { id } = match.params; + const { id, type } = match.params; const isApply = `${config.GIGS_PAGES_PATH}/${id}/apply` === match.url; const title = 'Gig Work | Topcoder Community | Topcoder'; const description = 'Compete and build up your profiles and skills! Topcoder members become eligible to work on Gig Work projects by first proving themselves in various skill sets through Topcoder competitions.'; - return ( + const inner = (