diff --git a/Dockerfile b/Dockerfile index 72ffa4d939..6345c8ee95 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,6 +73,7 @@ ARG GSHEETS_API_KEY ARG SENDGRID_API_KEY ARG GROWSURF_API_KEY ARG GROWSURF_CAMPAIGN_ID +ARG GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY # Optimizely ARG OPTIMIZELY_SDK_KEY @@ -137,6 +138,7 @@ ENV SENDGRID_API_KEY=$SENDGRID_API_KEY ENV GROWSURF_API_KEY=$GROWSURF_API_KEY ENV GROWSURF_CAMPAIGN_ID=$GROWSURF_CAMPAIGN_ID ENV GSHEETS_API_KEY=$GSHEETS_API_KEY +ENV GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY=$GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY # Optimizely ENV OPTIMIZELY_SDK_KEY=$OPTIMIZELY_SDK_KEY diff --git a/build.sh b/build.sh index 23730d8013..2440e90b4f 100755 --- a/build.sh +++ b/build.sh @@ -52,6 +52,7 @@ docker build -t $TAG \ --build-arg GSHEETS_API_KEY=$GSHEETS_API_KEY \ --build-arg OPTIMIZELY_SDK_KEY=$OPTIMIZELY_SDK_KEY \ --build-arg COMMUNITY_APP_URL=$COMMUNITY_APP_URL \ + --build-arg GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY=$GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY \ --build-arg VALID_ISSUERS=$VALID_ISSUERS . # Copies "node_modules" from the created image, if necessary for caching. diff --git a/config/custom-environment-variables.js b/config/custom-environment-variables.js index 75169457dd..e82ca22da4 100644 --- a/config/custom-environment-variables.js +++ b/config/custom-environment-variables.js @@ -113,4 +113,5 @@ module.exports = { OPTIMIZELY: { SDK_KEY: 'OPTIMIZELY_SDK_KEY', }, + GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: 'GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY', }; diff --git a/config/default.js b/config/default.js index bbf8b30a55..d09390c336 100644 --- a/config/default.js +++ b/config/default.js @@ -263,10 +263,13 @@ module.exports = { GROWSURF_COOKIE_SETTINGS: { secure: true, domain: '', - expires: 7, // days + expires: 30, // days }, GSHEETS_API_KEY: 'AIzaSyBRdKySN5JNCb2H6ZxJdTTvp3cWU51jiOQ', + GOOGLE_SERVICE_ACCOUNT_EMAIL: 'communityappserviceacc@tc-sheets-to-contentful.iam.gserviceaccount.com', + GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY: '', + GIG_REFERRALS_SHEET: '1xilx7NxDAvzAzOTbPpvb3lL3RWv1VD5W24OEMAoF9HU', AUTH_CONFIG: { AUTH0_URL: 'TC_M2M_AUTH0_URL', diff --git a/src/assets/images/icon-facebook.svg b/src/assets/images/icon-facebook.svg index ec2593813f..d89aeb95b6 100644 --- a/src/assets/images/icon-facebook.svg +++ b/src/assets/images/icon-facebook.svg @@ -1,8 +1,7 @@ - 1EF461D5-8CE7-4936-91E5-80EBD141882D - Created with sketchtool. + Share on Facebook diff --git a/src/assets/images/icon-linkedIn.svg b/src/assets/images/icon-linkedIn.svg index d7fbf67eaa..72d1f181fb 100644 --- a/src/assets/images/icon-linkedIn.svg +++ b/src/assets/images/icon-linkedIn.svg @@ -1,8 +1,7 @@ - A17EE70C-4FF2-418A-A9EE-80C7482C7231 - Created with sketchtool. + Share on LinkedIn diff --git a/src/assets/images/icon-twitter.svg b/src/assets/images/icon-twitter.svg index d11f28d598..9d22d794bf 100644 --- a/src/assets/images/icon-twitter.svg +++ b/src/assets/images/icon-twitter.svg @@ -1,8 +1,7 @@ - 98B36CB6-9363-4AE1-9293-675C31608E4A - Created with sketchtool. + Share on Twitter diff --git a/src/server/routes/gSheet.js b/src/server/routes/gSheet.js index 432dbcfb7b..81275d7249 100644 --- a/src/server/routes/gSheet.js +++ b/src/server/routes/gSheet.js @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ /** * The routes related to GSheets integration */ @@ -14,6 +15,7 @@ const routes = express.Router(); routes.use(cors()); routes.options('*', cors()); -routes.get('/:id', (req, res) => new GSheetService().getSheet(req, res)); +routes.get('/:id', (req, res) => new GSheetService().getSheetAPI(req, res)); +// routes.post('/:id', (req, res) => new GSheetService().addToSheetAPI(req, res)); // Enable it for API access to gsheets editing when needed export default routes; diff --git a/src/server/services/gSheet.js b/src/server/services/gSheet.js index a54d6428c4..33dadea03a 100644 --- a/src/server/services/gSheet.js +++ b/src/server/services/gSheet.js @@ -25,12 +25,18 @@ const getCircularReplacer = () => { * APIs in the same uniform manner. */ export default class GSheetService { + constructor() { + this.getSheetAPI = this.getSheetAPI.bind(this); + this.addToSheetAPI = this.getSheetAPI.bind(this); + this.addToSheet = this.getSheetAPI.bind(this); + } + /** * getSheet * @param {Object} req the request * @param {Object} res the response */ - async getSheet(req, res) { + async getSheetAPI(req, res) { const { index } = req.query; const { id } = req.params; const doc = new GoogleSpreadsheet(id); @@ -45,8 +51,85 @@ export default class GSheetService { rows: JSON.parse(rowsJson), }); } catch (e) { - res.status((e.response && e.response.status) || 500); + const status = (e.response && e.response.status) || 500; + if (status === 429) { + // rate limit issue - wait 15sec and retry + await new Promise(resolve => setTimeout(resolve, 15000)); + return this.getSheetAPI(req, res); + } + res.status(status); return res.send((e.response && e.response.data) || { ...e, message: e.message }); } } + + /** + * Adds rows to gsheet by ID + * Needs to be shared with the service account to work + * This is the controler method with req/res objects + * @param {Object} req the request + * @param {Object} res the response + */ + async addToSheetAPI(req, res) { + const { index } = req.query; + const { id } = req.params; + const doc = new GoogleSpreadsheet(id); + try { + // set credentials for working + await doc.useServiceAccountAuth({ + client_email: config.GOOGLE_SERVICE_ACCOUNT_EMAIL, + private_key: config.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY.replace(/\\m/g, '\n'), + }); + // load doc infos + await doc.loadInfo(); + // get 1st sheet + const sheet = doc.sheetsByIndex[index || 0]; + const moreRows = await sheet.addRows(req.body); + const rowsJson = JSON.stringify(moreRows, getCircularReplacer()); + return res.send({ + rows: JSON.parse(rowsJson), + }); + } catch (e) { + const status = (e.response && e.response.status) || 500; + if (status === 429) { + // rate limit issue - wait 15sec and retry + await new Promise(resolve => setTimeout(resolve, 15000)); + return this.addToSheetAPI(req, res); + } + res.status(status); + return res.send((e.response && e.response.data) || { ...e, message: e.message }); + } + } + + /** + * Adds rows to gsheet by ID + * Needs to be shared with the service account to work + * @param {string} id the doc id + * @param {Array} paylod the body to send + * @param {string} index sheet index in the doc. Defaults to 0 + */ + async addToSheet(id, payload, index = 0) { + const doc = new GoogleSpreadsheet(id); + try { + // set credentials for working + await doc.useServiceAccountAuth({ + client_email: config.GOOGLE_SERVICE_ACCOUNT_EMAIL, + private_key: config.GOOGLE_SERVICE_ACCOUNT_PRIVATE_KEY.replace(/\\m/g, '\n'), + }); + // load doc infos + await doc.loadInfo(); + // get 1st sheet + const sheet = doc.sheetsByIndex[index || 0]; + const moreRows = await sheet.addRows(payload); + const rowsJson = JSON.stringify(moreRows, getCircularReplacer()); + return rowsJson; + } catch (e) { + const status = (e.response && e.response.status) || 500; + if (status === 429) { + // rate limit issue - wait 15sec and retry + await new Promise(resolve => setTimeout(resolve, 15000)); + return this.addToSheet(id, payload, index); + } + return e; + } + } } diff --git a/src/server/services/growsurf.js b/src/server/services/growsurf.js index 920541b84a..1ede169b40 100644 --- a/src/server/services/growsurf.js +++ b/src/server/services/growsurf.js @@ -20,6 +20,30 @@ export default class GrowsurfService { }; } + /** + * Gets get participant. + * @return {Promise} + * @param {String} idOrEmail growsurf id or email + */ + async getParticipantByIdOREmail(idOrEmail) { + const response = await fetch(`${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${idOrEmail}`, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + Authorization: this.private.authorization, + }, + }); + if (response.status >= 300) { + return { + error: await response.json(), + code: response.status, + url: `${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${idOrEmail}`, + }; + } + const data = await response.json(); + return data; + } + /** * Gets get participant by email or id. * @return {Promise} @@ -67,7 +91,6 @@ export default class GrowsurfService { code: response.status, url: `${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant`, body, - private: this.private, // to remove in final release }; } const data = await response.json(); @@ -96,4 +119,31 @@ export default class GrowsurfService { } return result; } + + /** + * Update participant in growSurf + * @param {string} idOrEmail id or email + * @param {string} body payload + * @returns {Promise} + */ + async updateParticipant(idOrEmail, body) { + const response = await fetch(`${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${idOrEmail}`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: this.private.authorization, + }, + body, + }); + if (response.status >= 300) { + return { + error: await response.json(), + code: response.status, + url: `${this.private.baseUrl}/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${idOrEmail}`, + body, + }; + } + const data = await response.json(); + return data; + } } diff --git a/src/server/services/recruitCRM.js b/src/server/services/recruitCRM.js index 8eb1b0fc51..0fd4b66a6a 100644 --- a/src/server/services/recruitCRM.js +++ b/src/server/services/recruitCRM.js @@ -9,6 +9,7 @@ import { logger } from 'topcoder-react-lib'; import Joi from 'joi'; import GrowsurfService from './growsurf'; import { sendEmailDirect } from './sendGrid'; +// import GSheetService from './gSheet'; const FormData = require('form-data'); @@ -270,34 +271,58 @@ export default class RecruitCRMService { fileData.append('resume', file.buffer, file.originalname); } let candidateSlug; + let isNewCandidate = true; + let isReferred = false; let referralCookie = req.cookies[config.GROWSURF_COOKIE]; if (referralCookie) referralCookie = JSON.parse(referralCookie); + const tcHandle = _.findIndex(form.custom_fields, { field_id: 2 }); + let growRes; try { // referral tracking via growsurf - if (referralCookie && referralCookie.gigId === id) { + if (referralCookie) { const gs = new GrowsurfService(); - const tcHandle = _.findIndex(form.custom_fields, { field_id: 2 }); - const growRes = await gs.addParticipant(JSON.stringify({ - email: form.email, - referredBy: referralCookie.referralId, - referralStatus: 'CREDIT_PENDING', - firstName: form.first_name, - lastName: form.last_name, - metadata: { - gigId: id, - tcHandle: form.custom_fields[tcHandle].value, - }, - })); - // If everything set in Growsurf - // add referral link to candidate profile in recruitCRM - if (!growRes.error) { - form.custom_fields.push({ - field_id: 6, value: `https://app.growsurf.com/dashboard/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${growRes.id}`, - }); + // check if candidate exists in growsurf + const existRes = await gs.getParticipantByIdOREmail(form.email); + if (existRes.id) { + // candidate exists in growsurf + // update candidate to set referrer only if it is not set already + if (!existRes.referrer) { + growRes = await gs.updateParticipant(form.email, JSON.stringify({ + referredBy: referralCookie.referralId, + referralStatus: 'CREDIT_PENDING', + metadata: { + gigID: id, + }, + })); + // add referral link to candidate profile in recruitCRM + if (!growRes.error) { + isReferred = true; + form.custom_fields.push({ + field_id: 6, value: `https://app.growsurf.com/dashboard/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${growRes.id}`, + }); + } + } } else { - notifyKirilAndNick(growRes); + growRes = await gs.addParticipant(JSON.stringify({ + email: form.email, + referredBy: referralCookie.referralId, + referralStatus: 'CREDIT_PENDING', + firstName: form.first_name, + lastName: form.last_name, + metadata: { + gigId: id, + tcHandle: form.custom_fields[tcHandle].value, + }, + })); + // add referral link to candidate profile in recruitCRM + if (!growRes.error) { + isReferred = true; + form.custom_fields.push({ + field_id: 6, value: `https://app.growsurf.com/dashboard/campaign/${config.GROWSURF_CAMPAIGN_ID}/participant/${growRes.id}`, + }); + } } - // clear the cookie + // finally, clear the cookie res.cookie(config.GROWSURF_COOKIE, '', { maxAge: 0, overwrite: true, @@ -326,6 +351,7 @@ export default class RecruitCRMService { // Candidate exists in recruitCRM // We will update profile fields, otherwise we create new candidate below // Check if candidate is placed in gig currently + isNewCandidate = false; const candStatusIndex = _.findIndex( candidateData.data[0].custom_fields, { field_id: 12 }, ); @@ -450,6 +476,37 @@ export default class RecruitCRMService { notifyKirilAndNick(error); return res.send(error); } + // For new candidates that apply via referral link + // aka triggered referral state step 1 - notify and etc. housekeeping tasks + if (isNewCandidate && isReferred && !growRes.error) { + // update the tracking sheet + // enable that code when issue with service account key structure is resolved + // const gs = new GSheetService(); + // await gs.addToSheet(config.GIG_REFERRALS_SHEET, [[ + // `${form.first_name} ${form.last_name}`, + // form.email, + // `https://app.recruitcrm.io/candidate/${candidateData.slug}`, + // `https://topcoder.com/members/${form.custom_fields[tcHandle].value}`, + // `https://app.growsurf.com/dashboard/campaign/u9frbx/participant/${growRes.referrer.id}`, + // `${growRes.referrer.firstName} ${growRes.referrer.lastName}`, + // growRes.referrer.email, + // `https://topcoder.com/members/${growRes.referrer.metadata.tcHandle}`, + // `https://app.recruitcrm.io/job/${id}`, + // ]]); + // Notify the person who referred + sendEmailDirect({ + personalizations: [ + { + to: [{ email: growRes.referrer.email }], + subject: 'Thanks for your Topcoder referral!', + }, + ], + from: { email: 'noreply@topcoder.com', name: 'The Topcoder Community Team' }, + content: [{ + type: 'text/html', value: `

Hello ${growRes.referrer.metadata.tcHandle},

You just made our day! Sharing a Topcoder Gig Work opportunity is the BEST compliment you can give us. So pat yourself on the back, give yourself a hi-five, or just stand up and dance like no one is watching. You deserve it!

Did you know many of our Topcoder members consider earning through Topcoder to be life changing? There must be something in the air. You have just taken the first step into helping someone you know change their life for the better.

Because of that, we are excited to reward you. $500 is earned for every referral you send us that gets the gig. Learn more here.

Thank you for sharing Topcoder Gigs and hope to see you around here again soon!

- The Topcoder Community Team

`, + }], + }); + } // respond to API call const data = await applyResponse.json(); return res.send(data); diff --git a/src/shared/actions/growSurf.js b/src/shared/actions/growSurf.js new file mode 100644 index 0000000000..b3cbf3571d --- /dev/null +++ b/src/shared/actions/growSurf.js @@ -0,0 +1,59 @@ +/** + * Actions related to growsurf (gig referrals) + */ +/* global fetch */ +import { redux, config } from 'topcoder-react-utils'; + +const PROXY_ENDPOINT = `${config.URL.COMMUNITY_APP}/api`; + +/** + * Fetch init + */ +function getReferralIdInit() { + return { + loading: true, + }; +} + +/** + * Get referral id for the logged user + * if this member does not exist in growsurf it creates it in the system + */ +async function getReferralIdDone(profile) { + if (profile.email) { + const res = await fetch(`${PROXY_ENDPOINT}/growsurf/participants?participantId=${profile.email}`, { + method: 'POST', + body: JSON.stringify({ + email: profile.email, + firstName: profile.firstName, + lastName: profile.lastName, + tcHandle: profile.handle, + }), + headers: { + 'Content-Type': 'application/json', + }, + }); + if (res.status >= 300) { + return { + error: true, + details: await res.json(), + }; + } + const data = await res.json(); + return { + data, + }; + } + // no referral id without email + return { + error: true, + details: 'No profile email found', + }; +} + +export default redux.createActions({ + GROWSURF: { + GET_REFERRALID_INIT: getReferralIdInit, + GET_REFERRALID_DONE: getReferralIdDone, + }, +}); diff --git a/src/shared/components/Gigs/GigDetails/index.jsx b/src/shared/components/Gigs/GigDetails/index.jsx index 6124baadca..074cd79ab0 100644 --- a/src/shared/components/Gigs/GigDetails/index.jsx +++ b/src/shared/components/Gigs/GigDetails/index.jsx @@ -5,12 +5,15 @@ */ import { isEmpty } from 'lodash'; -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import PT from 'prop-types'; +import { connect } from 'react-redux'; import { isomorphy, Link, config } from 'topcoder-react-utils'; +import { PrimaryButton } from 'topcoder-react-ui-kit'; import ReactHtmlParser from 'react-html-parser'; import { getSalaryType, getCustomField } from 'utils/gigs'; import SubscribeMailChimpTag from 'containers/SubscribeMailChimpTag'; +import { isValidEmail } from 'utils/tc'; import './style.scss'; import IconFacebook from 'assets/images/icon-facebook.svg'; import IconTwitter from 'assets/images/icon-twitter.svg'; @@ -39,14 +42,17 @@ const ReactHtmlParserOptions = { }, }; -export default function GigDetails(props) { +function GigDetails(props) { const { - job, application, profile, onSendClick, isReferrSucess, formData, formErrors, onFormInputChange, isReferrError, getReferralId, referralId, onReferralDone, + job, application, profile, onSendClick, isReferrSucess, isReferrError, onReferralDone, growSurf, } = props; let shareUrl; let retUrl; if (isomorphy.isClientSide()) { shareUrl = encodeURIComponent(window.location.href); + if (growSurf && growSurf.data) { + shareUrl = `${window.location.origin}${window.location.pathname}?referralId=${growSurf.data.id}`; + } retUrl = `${window.location.origin}${window.location.pathname}/apply${window.location.search}`; } let skills = getCustomField(job.custom_fields, 'Technologies Required'); @@ -56,12 +62,10 @@ export default function GigDetails(props) { const [isModalOpen, setModalOpen] = useState(false); const [isLoginModalOpen, setLoginModalOpen] = useState(false); - let inputRef; + const [copyBtnText, setCopyBtnText] = useState('COPY'); + const [referrEmail, setreferrEmail] = useState(); const duration = getCustomField(job.custom_fields, 'Duration'); - - useEffect(() => { - if (referralId && formData.email && isEmpty(formErrors)) onSendClick(); - }, [referralId]); + let refEmailInput; return (
@@ -153,34 +157,79 @@ export default function GigDetails(props) {
-
- Share this job on:   - - - - - - - - - -
REFER THIS GIG
+ { + growSurf && growSurf.data ? ( + + Share your Referral Link: + +
+ { + const copyhelper = document.createElement('input'); + copyhelper.className = 'copyhelper'; + document.body.appendChild(copyhelper); + copyhelper.value = `https://www.topcoder.com/gigs/${job.slug}?referralId=${growSurf.data.id}`; + copyhelper.select(); + document.execCommand('copy'); + document.body.removeChild(copyhelper); + setCopyBtnText('COPIED'); + setTimeout(() => { + setCopyBtnText('COPY'); + }, 3000); + }} + > + {copyBtnText} + +
+ Share on:   + + + + + + + + + +
+
+
+ ) : ( +
+ Share this job on:   + + + + + + + + + +
+ ) + } +
+
+ or +
+

Refer someone to this gig and earn $500. Just add their email below. See how it works.

- onFormInputChange('email', e.target.value)} ref={el => inputRef = el} /> + setreferrEmail(e.target.value)} ref={ref => refEmailInput = ref} />
@@ -216,9 +265,8 @@ export default function GigDetails(props) { onCloseButton={() => setModalOpen(false)} isReferrSucess={isReferrSucess} isReferrError={isReferrError} - referralId={referralId} + referralId={growSurf && growSurf.data ? growSurf.data.id : null} onReferralDone={() => { - inputRef.value = ''; onReferralDone(); setModalOpen(false); }} @@ -241,7 +289,7 @@ export default function GigDetails(props) { GigDetails.defaultProps = { application: null, profile: {}, - referralId: null, + growSurf: {}, isReferrError: null, }; @@ -251,11 +299,18 @@ GigDetails.propTypes = { profile: PT.shape(), onSendClick: PT.func.isRequired, isReferrSucess: PT.bool.isRequired, - formErrors: PT.shape().isRequired, - formData: PT.shape().isRequired, - onFormInputChange: PT.func.isRequired, isReferrError: PT.shape(), - getReferralId: PT.func.isRequired, - referralId: PT.string, onReferralDone: PT.func.isRequired, + growSurf: PT.shape(), }; + +function mapStateToProps(state) { + const { growSurf } = state; + return { + growSurf, + }; +} + +export default connect( + mapStateToProps, +)(GigDetails); diff --git a/src/shared/components/Gigs/GigDetails/style.scss b/src/shared/components/Gigs/GigDetails/style.scss index 741e6c91db..63e6036d04 100644 --- a/src/shared/components/Gigs/GigDetails/style.scss +++ b/src/shared/components/Gigs/GigDetails/style.scss @@ -1,5 +1,7 @@ +/* stylelint-disable no-descending-specificity */ @import '~styles/mixins'; @import "~components/Contentful/default"; +@import "~components/buttons/themed/tc"; @mixin primaryBtn { background-color: #137d60; @@ -135,7 +137,8 @@ .shareButtons { display: flex; align-items: center; - margin-bottom: 12px; + margin: 20px 0 12px; + color: #fff; a { margin-right: 5px; @@ -144,12 +147,16 @@ margin-right: 0; } } + + svg > path { + fill: #fff; + } } .referr-area { background-image: linear-gradient(135.29deg, #2c95d7 0%, #06d6a0 100%); border-radius: 10px; - padding: 25px 32px 30px 20px; + padding: 25px 20px 30px 20px; margin-bottom: 20px; h6 { @@ -162,6 +169,69 @@ margin-bottom: 20px; } + .referralLinkTitile { + font-weight: 500; + color: #fff; + display: block; + margin: 15px 0 5px; + } + + .referralLink { + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 2px; + color: #fff; + width: 100%; + overflow: auto; + padding: 7px; + background: transparent; + margin: 0; + } + + .copyAndShare { + display: flex; + align-items: center; + justify-content: space-between; + margin: 15px 0 4px; + + @media screen and (max-width: 375px) { + flex-direction: column; + align-items: flex-start; + } + + button { + @include primary-borderless; + @include md; + + &:hover { + @include primary-borderless; + } + } + + .shareButtons { + margin: 0; + } + } + + .sepWrap { + display: flex; + align-items: center; + margin: 15px 0; + + .sepLine { + height: 1px; + background-color: #000; + opacity: 0.13; + flex: 2; + } + + span { + margin: 0 9px; + display: inline-block; + color: #fff; + font-weight: 500; + } + } + .referr-form { display: flex; justify-content: center; diff --git a/src/shared/components/Gigs/ReferralCode/index.jsx b/src/shared/components/Gigs/ReferralCode/index.jsx new file mode 100644 index 0000000000..03783f2c04 --- /dev/null +++ b/src/shared/components/Gigs/ReferralCode/index.jsx @@ -0,0 +1,108 @@ +/* eslint-disable max-len */ +/** + * Connects the Redux store to the GigsPages component. + */ +import React, { useState, useEffect } from 'react'; +import PT from 'prop-types'; +import _ from 'lodash'; +import { PrimaryButton } from 'topcoder-react-ui-kit'; +import LoadingIndicator from 'components/LoadingIndicator'; +import tc from 'components/buttons/themed/tc.scss'; +import ReferralModal from '../ReferralModal'; +import defautlStyle from './style.scss'; + +/** Themes for buttons + * those overwrite PrimaryButton style to match achieve various styles. + * Should implement pattern of classes. + */ +const buttonThemes = { + tc, +}; + +function ReferralCode(props) { + const { profile, growSurf } = props; + const [loginModalOpen, setLoginModalOpen] = useState(false); + const [growSurfState, setGrowSurfState] = useState(growSurf); + const [copyBtnText, setCopyBtnText] = useState('COPY'); + useEffect(() => { + setGrowSurfState(props.growSurf); + }, [growSurf]); + + return ( +
+ Topcoder Referral Program: + { + _.isEmpty(profile) ? ( + + Do you know someone who is perfect for a gig? You could earn $500 for referring them! + { + setLoginModalOpen(true); + }} + theme={{ + button: buttonThemes.tc['primary-borderless-sm'], + }} + > + REFFER A FRIEND + + { + loginModalOpen + && ( + setLoginModalOpen(false)} + isReferrSucess={false} + isReferrError={false} + onReferralDone={() => { }} + /> + ) + } + + ) : ( + + Your referral link: + { + growSurfState.data ? ( +
+ {`https://www.topcoder.com/gigs?referralId=${growSurfState.data.id}`} + { + const copyhelper = document.createElement('input'); + copyhelper.className = 'copyhelper'; + document.body.appendChild(copyhelper); + copyhelper.value = `https://www.topcoder.com/gigs?referralId=${growSurfState.data.id}`; + copyhelper.select(); + document.execCommand('copy'); + document.body.removeChild(copyhelper); + setCopyBtnText('COPIED'); + setTimeout(() => { + setCopyBtnText('COPY'); + }, 3000); + }} + theme={{ + button: buttonThemes.tc['primary-borderless-xs'], + }} + > + {copyBtnText} + +
+ ) : + } +
+ ) + } +
+ ); +} + +ReferralCode.defaultProps = { + profile: {}, + growSurf: {}, +}; + +ReferralCode.propTypes = { + profile: PT.shape(), + growSurf: PT.shape(), +}; + +export default ReferralCode; diff --git a/src/shared/components/Gigs/ReferralCode/style.scss b/src/shared/components/Gigs/ReferralCode/style.scss new file mode 100644 index 0000000000..e0b3561226 --- /dev/null +++ b/src/shared/components/Gigs/ReferralCode/style.scss @@ -0,0 +1,103 @@ +.container, +.containerWithLink { + display: flex; + background-image: linear-gradient(97.21deg, #2984bd 0%, #0ab88a 100%); + border-radius: 10px; + width: 100%; + max-width: 1280px; + margin: 30px auto 0; + align-items: center; + padding: 13px 25px 11px; + + @media screen and (max-width: 1280px) { + margin: 30px 15px 0; + width: auto; + } + + @media screen and (max-width: 768px) { + flex-direction: column; + align-items: flex-start; + } + + .title { + font-weight: bold; + + @media screen and (max-width: 768px) { + margin-bottom: 10px; + } + } + + span { + font-family: Roboto, sans-serif; + font-size: 18px; + color: #fff; + line-height: 22px; + margin-right: 5px; + } + + button { + margin-left: 10px !important; + + &:hover { + margin-left: 10px !important; + } + + @media screen and (max-width: 768px) { + margin: 10px 0 0 !important; + + &:hover { + margin: 10px 0 0 !important; + } + } + } + + .rondedArea { + border: 1px solid rgba(255, 255, 255, 0.4); + border-radius: 2px; + height: 35px; + display: flex; + align-items: center; + padding: 0 12px; + + @media screen and (max-width: 768px) { + max-width: 100%; + flex-direction: column; + align-items: flex-start; + position: relative; + width: 100%; + height: auto; + padding: 6px; + margin-bottom: 40px; + } + + span { + white-space: nowrap; + + @media screen and (max-width: 768px) { + max-width: 100%; + display: block; + overflow: auto; + margin-right: 0; + } + } + + button { + @media screen and (max-width: 768px) { + position: absolute; + bottom: -30px; + margin: 0 !important; + left: 0; + } + } + } +} + +.containerWithLink { + padding: 10px 25px 9px; + + svg { + width: 34px; + height: 34px; + margin: initial; + } +} diff --git a/src/shared/components/Gigs/ReferralModal/index.jsx b/src/shared/components/Gigs/ReferralModal/index.jsx index 25820d8c44..4c9e8f39f4 100644 --- a/src/shared/components/Gigs/ReferralModal/index.jsx +++ b/src/shared/components/Gigs/ReferralModal/index.jsx @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ /** * The modal used for gig referral flow */ @@ -41,7 +42,7 @@ function ReferralModal({ { !isEmpty(profile) ? (
{ - !referralId && !isReferrError && ( + referralId && !isReferrError && !isReferrSucess && (

Sending your referral...

@@ -90,10 +91,9 @@ function ReferralModal({
) : (
-

WARNING

-

You must be a Topcoder member to refer!

+

REFERRAL PROGRAM

+

Please login to receive your referral code.

- FIND OUT MORE { window.location = `${config.URL.AUTH}/member?retUrl=${encodeURIComponent(retUrl)}`; @@ -104,8 +104,9 @@ function ReferralModal({ > LOGIN + REGISTER
-

Not a member? It is free to register!

+

Find out how the referral program works here.

)} diff --git a/src/shared/components/Gigs/ReferralModal/modal.scss b/src/shared/components/Gigs/ReferralModal/modal.scss index 47ebdfb624..27a6bbd61e 100644 --- a/src/shared/components/Gigs/ReferralModal/modal.scss +++ b/src/shared/components/Gigs/ReferralModal/modal.scss @@ -26,7 +26,7 @@ } .loginMsg { - color: #ef476f; + color: #2a2a2a; font-size: 24px; line-height: 36px; margin-bottom: 40px; diff --git a/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx index 31d1e56f48..5594841ec9 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobDetails.jsx @@ -3,21 +3,17 @@ * driven by recruitCRM */ -import { isEmpty, trim } from 'lodash'; +import { isEmpty } from 'lodash'; import actions from 'actions/recruitCRM'; import LoadingIndicator from 'components/LoadingIndicator'; import GigDetails from 'components/Gigs/GigDetails'; import PT from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import { getQuery } from 'utils/url'; -import { isValidEmail } from 'utils/tc'; import { config } from 'topcoder-react-utils'; import fetch from 'isomorphic-fetch'; import RecruitCRMJobApply from './RecruitCRMJobApply'; -const cookies = require('browser-cookies'); - const PROXY_ENDPOINT = `${config.URL.COMMUNITY_APP}/api`; class RecruitCRMJobDetailsContainer extends React.Component { @@ -38,8 +34,6 @@ ${config.URL.BASE}${config.GIGS_PAGES_PATH}/${props.id}`, }; this.onSendClick = this.onSendClick.bind(this); - this.onFormInputChange = this.onFormInputChange.bind(this); - this.getReferralId = this.getReferralId.bind(this); this.onReferralDone = this.onReferralDone.bind(this); } @@ -53,65 +47,27 @@ ${config.URL.BASE}${config.GIGS_PAGES_PATH}/${props.id}`, if (isEmpty(job)) { getJob(id); } - const query = getQuery(); - if (query.referralId) { - cookies.set(config.GROWSURF_COOKIE, JSON.stringify({ - referralId: query.referralId, - gigId: id, - }), config.GROWSURF_COOKIE_SETTINGS); - } - } - - /** - * Form state setter - * @param {*} key key - * @param {*} update value - */ - onFormInputChange(key, update) { - this.setState((state) => { - const { formData, formErrors } = state; - if (key === 'email') { - formData.email = update; - if (trim(update)) { - if (!(isValidEmail(update))) formErrors.email = 'Invalid email'; - else { - delete formErrors.email; - } - } else formErrors.email = 'Email is required field'; - } - return { - formData, - formErrors, - }; - }); } /** * Send gig referral invite */ - async onSendClick() { - const { profile } = this.props; - const { formData, formErrors } = this.state; - if (!formData.email) { - formErrors.email = 'Email is required field'; - this.setState({ - formErrors, - }); - return; - } + async onSendClick(email) { + const { profile, growSurf } = this.props; + const { formData } = this.state; // email the invite const res = await fetch(`${PROXY_ENDPOINT}/mailchimp/email`, { method: 'POST', body: JSON.stringify({ personalizations: [ { - to: [{ email: formData.email }], + to: [{ email }], subject: `${profile.firstName} ${profile.lastName} Thinks This Topcoder Gig Is For You!`, }, ], from: { email: 'noreply@topcoder.com', name: `${profile.firstName} ${profile.lastName} via Topcoder Gigwork` }, content: [{ - type: 'text/plain', value: formData.body, + type: 'text/plain', value: `${formData.body}?referralId=${growSurf.data.id}`, }], }), headers: { @@ -142,43 +98,6 @@ ${config.URL.BASE}${config.GIGS_PAGES_PATH}/${props.id}`, }); } - /** - * Get referral id for the logged user - */ - async getReferralId() { - const { profile } = this.props; - const { referralId, formData } = this.state; - if (!referralId && profile.email) { - const res = await fetch(`${PROXY_ENDPOINT}/growsurf/participants?participantId=${profile.email}`, { - method: 'POST', - body: JSON.stringify({ - email: profile.email, - firstName: profile.firstName, - lastName: profile.lastName, - tcHandle: profile.handle, - }), - headers: { - 'Content-Type': 'application/json', - }, - }); - if (res.status >= 300) { - this.setState({ - isReferrError: { message: 'Failed to get your referralId.' }, - }); - } else { - const data = await res.json(); - formData.body += `?referralId=${data.id}`; - this.setState({ - referralId: data.id, - formData: { - ...formData, - body: formData.body, - }, - }); - } - } - } - render() { const { loading, @@ -186,13 +105,14 @@ ${config.URL.BASE}${config.GIGS_PAGES_PATH}/${props.id}`, isApply, application, profile, + growSurf, } = this.props; const { formErrors, formData, isReferrSucess, isReferrError, - referralId, + // referralId, } = this.state; if (loading) { @@ -210,11 +130,9 @@ ${config.URL.BASE}${config.GIGS_PAGES_PATH}/${props.id}`, isReferrSucess={isReferrSucess} formErrors={formErrors} formData={formData} - onFormInputChange={this.onFormInputChange} isReferrError={isReferrError} - getReferralId={this.getReferralId} - referralId={referralId} onReferralDone={this.onReferralDone} + growSurf={growSurf} /> ); } @@ -224,6 +142,7 @@ RecruitCRMJobDetailsContainer.defaultProps = { job: {}, application: null, profile: {}, + growSurf: {}, }; RecruitCRMJobDetailsContainer.propTypes = { @@ -234,16 +153,19 @@ RecruitCRMJobDetailsContainer.propTypes = { isApply: PT.bool.isRequired, application: PT.shape(), profile: PT.shape(), + growSurf: PT.shape(), }; function mapStateToProps(state, ownProps) { const data = state.recruitCRM[ownProps.id]; const profile = state.auth && state.auth.profile ? { ...state.auth.profile } : {}; + const { growSurf } = state; return { job: data ? data.job : {}, loading: data ? data.loading : true, application: data ? data.application : null, profile, + growSurf, }; } diff --git a/src/shared/containers/Gigs/RecruitCRMJobs.jsx b/src/shared/containers/Gigs/RecruitCRMJobs.jsx index 53ca6460cf..8bc3af8bd4 100644 --- a/src/shared/containers/Gigs/RecruitCRMJobs.jsx +++ b/src/shared/containers/Gigs/RecruitCRMJobs.jsx @@ -156,7 +156,7 @@ class RecruitCRMJobsContainer extends React.Component { if (loading) { return ( - ; +

Searching our database for the best gigs…

); diff --git a/src/shared/containers/GigsPages.jsx b/src/shared/containers/GigsPages.jsx index 44db1f7058..ad55a1ef42 100644 --- a/src/shared/containers/GigsPages.jsx +++ b/src/shared/containers/GigsPages.jsx @@ -14,6 +14,9 @@ import { OptimizelyProvider, createInstance } from '@optimizely/react-sdk'; import { connect } from 'react-redux'; import _ from 'lodash'; import { v4 as uuidv4 } from 'uuid'; +import { getQuery } from 'utils/url'; +import ReferralCode from 'components/Gigs/ReferralCode'; +import actions from 'actions/growSurf'; const optimizelyClient = createInstance({ sdkKey: config.OPTIMIZELY.SDK_KEY, @@ -21,7 +24,9 @@ const optimizelyClient = createInstance({ const cookies = require('browser-cookies'); function GigsPagesContainer(props) { - const { match, profile } = props; + const { + match, profile, growSurf, getReferralId, + } = props; const optProfile = { attributes: {}, }; @@ -30,6 +35,12 @@ function GigsPagesContainer(props) { optProfile.attributes.TC_Handle = profile.handle; optProfile.attributes.HomeCountryCode = profile.homeCountryCode; optProfile.attributes.email = profile.email; + // trigger referral id fetching when profile is loaded + if (isomorphy.isClientSide()) { + if (_.isEmpty(growSurf) || (!growSurf.loading && !growSurf.data)) { + getReferralId(profile); + } + } } else if (isomorphy.isClientSide()) { const idCookie = cookies.get('_tc.aid'); if (idCookie) { @@ -45,6 +56,15 @@ function GigsPagesContainer(props) { }); } } + // check for referral code in the URL and set it to cookie + if (isomorphy.isClientSide()) { + const query = getQuery(); + if (query.referralId) { + cookies.set(config.GROWSURF_COOKIE, JSON.stringify({ + referralId: query.referralId, + }), config.GROWSURF_COOKIE_SETTINGS); + } + } const { id, type } = match.params; const isApply = `${config.GIGS_PAGES_PATH}/${id}/apply` === match.url; const title = 'Gig Work | Topcoder Community | Topcoder'; @@ -76,10 +96,13 @@ window._chatlio = window._chatlio||[]; } { !id && !type ? ( - + + + + ) : null }