Skip to content
This repository was archived by the owner on Mar 13, 2025. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 7a865ea

Browse files
authoredJun 28, 2021
Merge pull request #140 from topcoder-platform/dev
My Gigs > Release of Profile Integration
2 parents cc96eae + 1987028 commit 7a865ea

File tree

26 files changed

+440
-196
lines changed

26 files changed

+440
-196
lines changed
 

‎.circleci/config.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,6 @@ workflows:
7777
branches:
7878
only:
7979
- dev
80-
- fix-m2m-env-var
8180

8281
# Production builds are exectuted only on tagged commits to the
8382
# master branch.

‎config/default.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ module.exports = {
1212
COMMUNITY_APP: "https://community-app.topcoder-dev.com",
1313
PLATFORM_WEBSITE_URL: "https://platform.topcoder-dev.com",
1414
},
15-
RECRUIT_API: process.env.RECRUIT_API,
15+
RECRUIT_API: "https://www.topcoder-dev.com",
1616
// the server api base path
1717
API_BASE_PATH: process.env.API_BASE_PATH || "/earn-app/api/my-gigs",
1818
// the log level, default is 'debug'

‎config/development.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ module.exports = {
1111
COMMUNITY_APP: "https://community-app.topcoder-dev.com",
1212
PLATFORM_WEBSITE_URL: "https://platform.topcoder-dev.com",
1313
},
14+
RECRUIT_API: "https://www.topcoder-dev.com",
1415
};

‎config/production.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,5 @@ module.exports = {
1111
COMMUNITY_APP: "https://community-app.topcoder.com",
1212
PLATFORM_WEBSITE_URL: "https://platform.topcoder.com",
1313
},
14+
RECRUIT_API: "https://www.topcoder.com",
1415
};

‎src/actions/lookup.js

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,35 @@ async function checkIsLoggedIn() {
1313
return service.checkIsLoggedIn();
1414
}
1515

16-
async function getGigStatuses() {
17-
return service.getGigStatuses();
16+
/**
17+
* Gets all the countries.
18+
* @returns {Array} Array containing all countries
19+
*/
20+
async function getAllCountries() {
21+
// fetch the first page to see how many more fetches are necessary to get all
22+
const countries = await service.getPaginatedCountries();
23+
const {
24+
meta: { totalPages },
25+
} = countries;
26+
27+
const pagesMissing = totalPages - 1;
28+
29+
// fetch the other pages.
30+
const allPageResults = await Promise.all(
31+
[...Array(pagesMissing > 0 ? pagesMissing : 0)].map((_, index) => {
32+
const newPage = index + 2;
33+
34+
return service.getPaginatedCountries(newPage);
35+
})
36+
);
37+
38+
const newCountries = allPageResults.map((data) => data).flat();
39+
return [...countries, ...newCountries];
1840
}
1941

2042
export default createActions({
2143
GET_TAGS: getTags,
2244
GET_COMMUNITY_LIST: getCommunityList,
2345
CHECK_IS_LOGGED_IN: checkIsLoggedIn,
24-
GET_GIG_STATUSES: getGigStatuses,
46+
GET_ALL_COUNTRIES: getAllCountries,
2547
});

‎src/api/app-routes.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,19 @@ const authenticator = require("tc-core-library-js").middleware.jwtAuthenticator;
1818
*/
1919
module.exports = (app) => {
2020
app.use(express.json());
21-
app.use(cors());
21+
app.use(
22+
cors({
23+
// Allow browsers access pagination data in headers
24+
exposedHeaders: [
25+
"X-Page",
26+
"X-Per-Page",
27+
"X-Total",
28+
"X-Total-Pages",
29+
"X-Prev-Page",
30+
"X-Next-Page",
31+
],
32+
})
33+
);
2234
app.use(
2335
fileUpload({
2436
limits: {

‎src/api/common/helper.js

Lines changed: 69 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,26 @@ async function getMember(handle, query) {
335335
return res.body;
336336
}
337337

338+
/**
339+
* Get member traits info
340+
* @param {string} handle the handle of the user
341+
* @param {string} query the query criteria
342+
* @returns the object of member traitsdetails
343+
*/
344+
async function getMemberTraits(handle, query) {
345+
const token = await getM2MToken();
346+
const url = `${config.API.V5}/members/${handle}/traits`;
347+
const res = await request
348+
.get(url)
349+
.query(query)
350+
.set("Authorization", `Bearer ${token}`)
351+
.set("Accept", "application/json");
352+
localLogger.debug({
353+
context: "getMemberTraits",
354+
message: `response body: ${JSON.stringify(res.body)}`,
355+
});
356+
return res.body;
357+
}
338358
/**
339359
* Update member details
340360
* @param {string} handle the handle of the user
@@ -357,6 +377,28 @@ async function updateMember(currentUser, data) {
357377
return res.body;
358378
}
359379

380+
/**
381+
* Update member traits
382+
* @param {string} handle the handle of the user
383+
* @param {object} data the data to be updated
384+
* @return {object} the object of updated member details
385+
*/
386+
async function updateMemberTraits(currentUser, data) {
387+
const token = currentUser.jwtToken;
388+
const url = `${config.API.V5}/members/${currentUser.handle}/traits`;
389+
const res = await request
390+
.put(url)
391+
.set("Authorization", token)
392+
.set("Content-Type", "application/json")
393+
.set("Accept", "application/json")
394+
.send(data);
395+
localLogger.debug({
396+
context: "updateMemberTraits",
397+
message: `response body: ${JSON.stringify(res.body)}`,
398+
});
399+
return res.body;
400+
}
401+
360402
/**
361403
* Get Recruit CRM profile details
362404
* @param {object} currentUser the user who performs the operation
@@ -379,21 +421,36 @@ async function getRCRMProfile(currentUser) {
379421
/**
380422
* Update Recruit CRM profile details
381423
* @param {object} currentUser the user who performs the operation
382-
* @param {object} file the resume file
383424
* @param {object} data the data to be updated
425+
* @param {object} file the resume file
384426
* @return {object} the returned object
385427
*/
386-
async function updateRCRMProfile(currentUser, file, data) {
428+
async function updateRCRMProfile(currentUser, data, file) {
387429
const token = currentUser.jwtToken;
388430
const url = `${config.RECRUIT_API}/api/recruit/profile`;
389-
const res = await request
390-
.post(url)
391-
.set("Authorization", token)
392-
.set("Content-Type", "multipart/form-data")
393-
.set("Accept", "application/json")
394-
.field("phone", data.phone)
395-
.field("availability", data.availability)
396-
.attach("resume", file.data, file.name);
431+
let res = null;
432+
if (file) {
433+
res = await request
434+
.post(url)
435+
.set("Authorization", token)
436+
.set("Content-Type", "multipart/form-data")
437+
.set("Accept", "application/json")
438+
.field("phone", data.phone)
439+
.field("availability", data.availability)
440+
.field("city", data.city)
441+
.field("countryName", data.countryName)
442+
.attach("resume", file.data, file.name);
443+
} else {
444+
res = await request
445+
.post(url)
446+
.set("Authorization", token)
447+
.set("Content-Type", "multipart/form-data")
448+
.set("Accept", "application/json")
449+
.field("phone", data.phone)
450+
.field("availability", data.availability)
451+
.field("city", data.city)
452+
.field("countryName", data.countryName);
453+
}
397454
localLogger.debug({
398455
context: "updateRCRMProfile",
399456
message: `response body: ${JSON.stringify(res.body)}`,
@@ -412,7 +469,9 @@ module.exports = {
412469
getJobCandidates,
413470
getJobs,
414471
getMember,
472+
getMemberTraits,
415473
updateMember,
474+
updateMemberTraits,
416475
getRCRMProfile,
417476
updateRCRMProfile,
418477
};

‎src/api/controllers/ProfileController.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ async function getMyProfile(req, res) {
1919
* @param res the response
2020
*/
2121
async function updateMyProfile(req, res) {
22-
await service.updateMyProfile(req.authUser, req.files, req.body);
22+
await service.updateMyProfile(req.authUser, req.body, req.files);
2323
res.status(204).end();
2424
}
2525

‎src/api/mock-api/mock-api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ app.use((req, res, next) => {
2121

2222
app.get("/api/recruit/profile", (req, res) => {
2323
const result = {
24+
hasProfile: false,
2425
phone: "555-555-55-55",
2526
resume: "https://resume.topcoder.com/1234567",
26-
availibility: true,
27+
availability: true,
2728
};
2829
res.status(200).json(result);
2930
});

‎src/api/services/ProfileService.js

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,10 @@ async function getMyProfile(currentUser) {
3131
email: _.get(member, "email", null),
3232
city: _.get(member, "addresses[0].city", null),
3333
country: _.get(member, "competitionCountryCode", null),
34+
hasProfile: _.get(recruitProfile, "hasProfile", false),
3435
phone: _.get(recruitProfile, "phone", null),
3536
resume: _.get(recruitProfile, "resume", null),
36-
availability: _.get(recruitProfile, "availibility", true),
37+
availability: _.get(recruitProfile, "availability", true),
3738
};
3839
}
3940

@@ -48,13 +49,13 @@ getMyProfile.schema = Joi.object()
4849
* @param {object} currentUser the user who perform this operation.
4950
* @param {object} data the data to be updated
5051
*/
51-
async function updateMyProfile(currentUser, files, data) {
52+
async function updateMyProfile(currentUser, data, files) {
5253
// we expect logged-in users
5354
if (currentUser.isMachine) {
5455
return;
5556
}
5657
// check if file was truncated
57-
if (files.resume.truncated) {
58+
if (files && files.resume.truncated) {
5859
throw new errors.BadRequestError(
5960
`Maximum allowed file size is ${config.MAX_ALLOWED_FILE_SIZE_MB} MB`
6061
);
@@ -64,7 +65,7 @@ async function updateMyProfile(currentUser, files, data) {
6465
`^.*\.(${_.join(config.ALLOWED_FILE_TYPES, "|")})$`,
6566
"i"
6667
);
67-
if (!regex.test(files.resume.name)) {
68+
if (files && !regex.test(files.resume.name)) {
6869
throw new errors.BadRequestError(
6970
`Allowed file types are: ${_.join(config.ALLOWED_FILE_TYPES, ",")}`
7071
);
@@ -75,56 +76,96 @@ async function updateMyProfile(currentUser, files, data) {
7576
"fields=addresses,competitionCountryCode,homeCountryCode"
7677
);
7778
const update = {};
79+
let shouldUpdateTrait = false;
7880
// update member data if city is different from existing one
7981
if (_.get(member, "addresses[0].city") !== data.city) {
8082
update.addresses = _.cloneDeep(member.addresses);
8183
if (!_.isEmpty(update.addresses)) {
8284
update.addresses[0].city = data.city;
8385
delete update.addresses[0].createdAt;
8486
delete update.addresses[0].updatedAt;
87+
delete update.addresses[0].createdBy;
88+
delete update.addresses[0].updatedBy;
89+
update.addresses[0].streetAddr1 = update.addresses[0].streetAddr1
90+
? update.addresses[0].streetAddr1
91+
: " ";
92+
update.addresses[0].streetAddr2 = update.addresses[0].streetAddr2
93+
? update.addresses[0].streetAddr2
94+
: " ";
95+
update.addresses[0].type = update.addresses[0].type
96+
? update.addresses[0].type
97+
: "HOME";
98+
update.addresses[0].stateCode = update.addresses[0].stateCode
99+
? update.addresses[0].stateCode
100+
: " ";
101+
update.addresses[0].zip = update.addresses[0].zip
102+
? update.addresses[0].zip
103+
: " ";
85104
} else {
86105
update.addresses = [
87106
{
88107
city: data.city,
108+
type: "HOME",
109+
stateCode: " ",
110+
zip: " ",
111+
streetAddr1: " ",
112+
streetAddr2: " ",
89113
},
90114
];
91115
}
92116
}
93117
// update member data if competitionCountryCode is different from existing one
94118
if (_.get(member, "competitionCountryCode") !== data.country) {
95119
update.competitionCountryCode = data.country;
120+
shouldUpdateTrait = true;
96121
}
97122
if (_.get(member, "homeCountryCode") !== data.country) {
98123
update.homeCountryCode = data.country;
124+
shouldUpdateTrait = true;
99125
}
100126
// avoid unnecessary api calls
101127
if (!_.isEmpty(update)) {
102128
await helper.updateMember(currentUser, update);
103129
}
104-
await helper.updateRCRMProfile(currentUser, files.resume, {
105-
phone: data.phone,
106-
availability: data.availability,
107-
});
130+
if (shouldUpdateTrait) {
131+
const memberTraits = await helper.getMemberTraits(
132+
currentUser.handle,
133+
`traitIds=basic_info`
134+
);
135+
if (memberTraits && memberTraits.length) {
136+
memberTraits[0]["traits"].data[0].country = data.countryName;
137+
delete memberTraits[0].createdAt;
138+
delete memberTraits[0].createdBy;
139+
delete memberTraits[0].updatedAt;
140+
delete memberTraits[0].updatedBy;
141+
delete memberTraits[0].userId;
142+
await helper.updateMemberTraits(currentUser, memberTraits);
143+
}
144+
}
145+
await helper.updateRCRMProfile(
146+
currentUser,
147+
{
148+
phone: data.phone,
149+
availability: data.availability,
150+
city: data.city,
151+
countryName: data.countryName,
152+
},
153+
files && files.resume
154+
);
108155
}
109156

110-
updateMyProfile.schema = Joi.object()
111-
.keys({
112-
currentUser: Joi.object().required(),
113-
files: Joi.object()
114-
.keys({
115-
resume: Joi.object().required(),
116-
})
117-
.required(),
118-
data: Joi.object()
119-
.keys({
120-
city: Joi.string().required(),
121-
country: Joi.string().required(),
122-
phone: Joi.string().required(),
123-
availability: Joi.boolean().required(),
124-
})
125-
.required(),
126-
})
127-
.required();
157+
updateMyProfile.schema = Joi.object({
158+
currentUser: Joi.object().required(),
159+
data: Joi.object()
160+
.keys({
161+
city: Joi.string().required(),
162+
country: Joi.string().required(),
163+
countryName: Joi.string().required(),
164+
phone: Joi.string().required(),
165+
availability: Joi.boolean().required(),
166+
})
167+
.required(),
168+
}).unknown();
128169

129170
module.exports = {
130171
getMyProfile,

‎src/assets/data/my-gigs.json

Lines changed: 0 additions & 27 deletions
This file was deleted.

‎src/components/Dropdown/index.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,8 +93,8 @@ Dropdown.defaultProps = {
9393
Dropdown.propTypes = {
9494
options: PT.arrayOf(
9595
PT.shape({
96-
label: PT.string.isRequired,
97-
selected: PT.bool.isRequired,
96+
label: PT.string,
97+
selected: PT.bool,
9898
})
9999
).isRequired,
100100
placeholder: PT.string,

‎src/constants/index.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,11 @@ export const MY_GIG_STATUS_PLACED = "PLACED";
343343
export const GIG_STATUS = {
344344
AVAILABLE: "Available",
345345
UNAVAILABLE: "Unavailable",
346-
PLACED: "Placed",
346+
};
347+
348+
export const GIG_STATUS_TOOLTIP = {
349+
AVAILABLE: "You’re open to take on new jobs.",
350+
UNAVAILABLE: "You’re not open to take on new jobs.",
347351
};
348352

349353
export const EMPTY_GIGS_TEXT =

‎src/containers/MyGigs/JobListing/JobCard/index.jsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,7 @@ const JobCard = ({ job }) => {
109109
</ul>
110110
</div>
111111
<div
112-
styleName={`right-side ${
113-
job.phaseAction === MY_GIG_PHASE_ACTION.STAND_BY ? "stand-by" : ""
114-
} ${!job.phaseAction ? "none" : ""}`}
112+
styleName={`right-side stand-by ${!job.phaseAction ? "none" : ""}`}
115113
>
116114
{job.phaseAction && <Button size="lg">{job.phaseAction}</Button>}
117115
</div>

‎src/containers/MyGigs/JobListing/JobCard/styles.scss

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@
2828
margin-bottom: 12px;
2929
line-height: 38px;
3030
text-transform: uppercase;
31+
text-overflow: ellipsis;
32+
max-width: 660px;
33+
overflow: hidden;
3134
}
3235
}
3336

‎src/containers/MyGigs/index.jsx

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,19 +19,18 @@ const MyGigs = ({
1919
total,
2020
numLoaded,
2121
profile,
22-
statuses,
2322
getProfile,
24-
getStatuses,
2523
updateProfile,
2624
updateProfileSuccess,
25+
getAllCountries,
2726
}) => {
2827
const propsRef = useRef();
29-
propsRef.current = { getMyGigs, getProfile, getStatuses };
28+
propsRef.current = { getMyGigs, getProfile, getAllCountries };
3029

3130
useEffect(() => {
3231
propsRef.current.getMyGigs();
3332
propsRef.current.getProfile();
34-
propsRef.current.getStatuses();
33+
propsRef.current.getAllCountries();
3534
}, []);
3635

3736
const [openUpdateProfile, setOpenUpdateProfile] = useState(false);
@@ -40,6 +39,8 @@ const MyGigs = ({
4039
useEffect(() => {
4140
if (updateProfileSuccess) {
4241
setOpenUpdateSuccess(true);
42+
// in case of success, let's fetch the updated profile
43+
propsRef.current.getProfile();
4344
}
4445
}, [updateProfileSuccess]);
4546

@@ -51,7 +52,7 @@ const MyGigs = ({
5152
<Button
5253
isPrimary
5354
size="lg"
54-
disabled={true}
55+
disabled={!(profile && profile.hasProfile)}
5556
onClick={() => {
5657
setOpenUpdateProfile(true);
5758
}}
@@ -74,7 +75,6 @@ const MyGigs = ({
7475
<Modal open={openUpdateProfile}>
7576
<UpdateGigProfile
7677
profile={profile}
77-
statuses={statuses}
7878
onSubmit={(profileEdit) => {
7979
updateProfile(profileEdit);
8080
setOpenUpdateProfile(false);
@@ -102,28 +102,26 @@ MyGigs.propTypes = {
102102
total: PT.number,
103103
numLoaded: PT.number,
104104
profile: PT.shape(),
105-
statuses: PT.arrayOf(PT.string),
106105
getProfile: PT.func,
107-
getStatuses: PT.func,
108106
updateProfile: PT.func,
109107
updateProfileSuccess: PT.bool,
108+
getAllCountries: PT.func,
110109
};
111110

112111
const mapStateToProps = (state) => ({
113112
myGigs: state.myGigs.myGigs,
114113
total: state.myGigs.total,
115114
numLoaded: state.myGigs.numLoaded,
116115
profile: state.myGigs.profile,
117-
statuses: state.lookup.gigStatuses,
118116
updateProfileSuccess: state.myGigs.updatingProfileSucess,
119117
});
120118

121119
const mapDispatchToProps = {
122120
getMyGigs: actions.myGigs.getMyGigs,
123121
loadMore: actions.myGigs.loadMoreMyGigs,
124122
getProfile: actions.myGigs.getProfile,
125-
getStatuses: actions.lookup.getGigStatuses,
126123
updateProfile: actions.myGigs.updateProfile,
124+
getAllCountries: actions.lookup.getAllCountries,
127125
};
128126

129127
export default connect(mapStateToProps, mapDispatchToProps)(MyGigs);

‎src/containers/MyGigs/modals/UpdateGigProfile/index.jsx

Lines changed: 126 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/* global process */
22
import React, { useEffect, useMemo, useState, useRef } from "react";
3+
import { connect } from "react-redux";
34
import PT from "prop-types";
45
import Button from "components/Button";
56
import FilePicker from "components/FilePicker";
@@ -9,36 +10,50 @@ import UserPhoto from "components/UserPhoto";
910
import IconClose from "assets/icons/close.svg";
1011
import IconInfo from "assets/icons/info.svg";
1112
import StatusTooltip from "./tooltips/StatusTooltip";
12-
import _ from "lodash";
13+
import actions from "../../../../actions";
14+
import _, { size, values } from "lodash";
1315

14-
import * as constants from "constants";
16+
import { GIG_STATUS } from "../../../../constants";
1517
import * as utils from "utils";
1618

1719
import "./styles.scss";
1820

19-
const UpdateGigProfile = ({ profile, statuses, onSubmit, onClose }) => {
21+
const UpdateGigProfile = ({
22+
profile,
23+
onSubmit,
24+
onClose,
25+
countries,
26+
getAllCountries,
27+
}) => {
28+
const statuses = values(GIG_STATUS);
2029
const countryOptions = useMemo(() => {
21-
const countryMap = utils.myGig.countries.getNames("en");
22-
const options = Object.keys(countryMap).map((key) => countryMap[key]);
23-
const selected = profile.country;
24-
return utils.createDropdownOptions(options, selected);
25-
}, [profile]);
26-
27-
const statusOptions = useMemo(() => {
28-
const selected = profile.status;
29-
const options =
30-
profile.gigStatus === constants.MY_GIG_STATUS_PLACED
31-
? statuses
32-
: statuses.filter((s) => s !== constants.GIG_STATUS.PLACED);
33-
return utils.createDropdownOptions(options, selected);
34-
}, [profile, statuses]);
30+
const selectedCountry = countries.find(
31+
(country) => country.countryCode === profile.country
32+
);
33+
return utils.createDropdownOptions(
34+
countries.map((country) => country.name),
35+
selectedCountry
36+
);
37+
}, [profile, countries]);
3538

3639
const [profileEdit, setProfileEdit] = useState(
3740
profile ? _.clone(profile) : null
3841
);
42+
43+
const statusOptions = useMemo(() => {
44+
const selected = profileEdit.status;
45+
const options = statuses.filter((s) => s !== GIG_STATUS.PLACED);
46+
return utils.createDropdownOptions(options, selected);
47+
}, [profileEdit, statuses]);
48+
3949
const [validation, setValidation] = useState(null);
4050
const [pristine, setPristine] = useState(true);
4151

52+
// only fetch countries when they don't exist in redux store.
53+
useEffect(() => {
54+
if (size(countries) === 0) getAllCountries();
55+
}, [countries, getAllCountries]);
56+
4257
useEffect(() => {
4358
setProfileEdit(_.clone(profile));
4459
}, [profile]);
@@ -61,10 +76,10 @@ const UpdateGigProfile = ({ profile, statuses, onSubmit, onClose }) => {
6176
validation.file = profileEdit.fileError;
6277
}
6378

64-
if (!profileEdit.file) {
65-
validation = validation || {};
66-
validation.file = "Please, pick your CV file for uploading";
67-
}
79+
// if (!profileEdit.file) {
80+
// validation = validation || {};
81+
// validation.file = "Please, pick your CV file for uploading";
82+
// }
6883

6984
if ((error = utils.myGig.validateCity(profileEdit.city))) {
7085
validation = validation || {};
@@ -103,10 +118,66 @@ const UpdateGigProfile = ({ profile, statuses, onSubmit, onClose }) => {
103118

104119
const onSubmitProfile = () => {
105120
const update = varsRef.current.profileEdit;
121+
if (!update.countryName) {
122+
const selectedCountry = countries.find(
123+
(country) => country.countryCode === update.country
124+
);
125+
update.countryName = selectedCountry.name;
126+
}
106127
delete update.fileError;
107128
onSubmit(update);
108129
};
109130

131+
const handleStatusDropdownChange = (newOptions) => {
132+
const selectedOption = utils.getSelectedDropdownOption(newOptions);
133+
setProfileEdit({
134+
...varsRef.current.profileEdit,
135+
status: selectedOption.label,
136+
});
137+
setPristine(false);
138+
};
139+
140+
const handleFilePick = (file, error) => {
141+
if (error) {
142+
setProfileEdit({
143+
...varsRef.current.profileEdit,
144+
file,
145+
fileError: error,
146+
uploadTime: null,
147+
});
148+
} else {
149+
setProfileEdit({
150+
...varsRef.current.profileEdit,
151+
file,
152+
fileError: null,
153+
uploadTime: null,
154+
});
155+
}
156+
setPristine(false);
157+
};
158+
159+
const handleCountryDropdownChange = (newOptions) => {
160+
const selectedOption = utils.getSelectedDropdownOption(newOptions);
161+
const country = countries.find(
162+
(country) => selectedOption.label === country.name
163+
);
164+
setProfileEdit({
165+
...varsRef.current.profileEdit,
166+
country: country.countryCode,
167+
countryName: country.name,
168+
});
169+
setPristine(false);
170+
};
171+
/**
172+
* Generic handler for inputs to update the correct value
173+
* @param {string} field
174+
* @returns
175+
*/
176+
const handleInputChange = (field) => (value) => {
177+
setProfileEdit({ ...varsRef.current.profileEdit, [field]: value });
178+
setPristine(false);
179+
};
180+
110181
return (
111182
<div styleName="update-resume">
112183
<button styleName="close" onClick={onClose}>
@@ -138,27 +209,27 @@ const UpdateGigProfile = ({ profile, statuses, onSubmit, onClose }) => {
138209
<Dropdown
139210
options={statusOptions}
140211
size="xs"
141-
onChange={(newOptions) => {
142-
const selectedOption = utils.getSelectedDropdownOption(
143-
newOptions
144-
);
145-
setProfileEdit({
146-
...varsRef.current.profileEdit,
147-
status: selectedOption.label,
148-
});
149-
setPristine(false);
150-
}}
212+
onChange={handleStatusDropdownChange}
151213
errorMsg={(!pristine && validation && validation.status) || ""}
152214
/>
153215
</div>
154-
<StatusTooltip>
216+
<StatusTooltip statuses={statuses}>
155217
<i styleName="icon">
156218
<IconInfo />
157219
</i>
158220
</StatusTooltip>
159221
</div>
160222
</div>
161223
<div styleName="details">
224+
{profile && profile.existingResume && (
225+
<div styleName="resume-details">
226+
Please upload your resume/CV. Double-check that all of your tech
227+
skills are listed in your resume/CV.&nbsp;&nbsp;&nbsp;
228+
<a href={profile.existingResume.file_link} target="_blank">
229+
{profile.existingResume.filename}
230+
</a>
231+
</div>
232+
)}
162233
<div styleName="resume">
163234
<FilePicker
164235
label="Drag & drop your resume or CV here - Please Omit Contact Information"
@@ -167,35 +238,15 @@ const UpdateGigProfile = ({ profile, statuses, onSubmit, onClose }) => {
167238
uploadTime={profileEdit.uploadTime}
168239
accept=".pdf, .docx"
169240
errorMsg={(!pristine && validation && validation.file) || ""}
170-
onFilePick={(file, error) => {
171-
if (error) {
172-
setProfileEdit({
173-
...varsRef.current.profileEdit,
174-
file,
175-
fileError: error,
176-
uploadTime: null,
177-
});
178-
} else {
179-
setProfileEdit({
180-
...varsRef.current.profileEdit,
181-
file,
182-
fileError: null,
183-
uploadTime: null,
184-
});
185-
}
186-
setPristine(false);
187-
}}
241+
onFilePick={handleFilePick}
188242
/>
189243
</div>
190244
<div styleName="city">
191245
<TextInput
192246
value={profileEdit.city}
193247
label="City"
194248
required
195-
onChange={(value) => {
196-
setProfileEdit({ ...varsRef.current.profileEdit, city: value });
197-
setPristine(false);
198-
}}
249+
onChange={handleInputChange("city")}
199250
errorMsg={(!pristine && validation && validation.city) || ""}
200251
/>
201252
</div>
@@ -204,16 +255,7 @@ const UpdateGigProfile = ({ profile, statuses, onSubmit, onClose }) => {
204255
options={countryOptions}
205256
label="Country"
206257
required
207-
onChange={(newOptions) => {
208-
const selectedOption = utils.getSelectedDropdownOption(
209-
newOptions
210-
);
211-
setProfileEdit({
212-
...varsRef.current.profileEdit,
213-
country: selectedOption.label,
214-
});
215-
setPristine(false);
216-
}}
258+
onChange={handleCountryDropdownChange}
217259
errorMsg={(!pristine && validation && validation.country) || ""}
218260
/>
219261
</div>
@@ -222,13 +264,7 @@ const UpdateGigProfile = ({ profile, statuses, onSubmit, onClose }) => {
222264
value={profileEdit.phone}
223265
label="Phone - Please have the Country Code Included"
224266
required
225-
onChange={(value) => {
226-
setProfileEdit({
227-
...varsRef.current.profileEdit,
228-
phone: value,
229-
});
230-
setPristine(false);
231-
}}
267+
onChange={handleInputChange("phone")}
232268
errorMsg={(!pristine && validation && validation.phone) || ""}
233269
/>
234270
</div>
@@ -256,9 +292,26 @@ const UpdateGigProfile = ({ profile, statuses, onSubmit, onClose }) => {
256292

257293
UpdateGigProfile.propTypes = {
258294
profile: PT.shape(),
259-
statuses: PT.arrayOf(PT.string),
260295
onSubmit: PT.func,
261296
onClose: PT.func,
262297
};
263298

264-
export default UpdateGigProfile;
299+
const mapStateToProps = (state) => ({
300+
countries: state.lookup.countries,
301+
});
302+
303+
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
304+
...ownProps,
305+
...stateProps,
306+
...dispatchProps,
307+
});
308+
309+
const mapDispatchToProps = {
310+
getAllCountries: actions.lookup.getAllCountries,
311+
};
312+
313+
export default connect(
314+
mapStateToProps,
315+
mapDispatchToProps,
316+
mergeProps
317+
)(UpdateGigProfile);

‎src/containers/MyGigs/modals/UpdateGigProfile/styles.scss

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@
6060
flex-direction: column;
6161
align-items: center;
6262
width: 260px;
63-
min-height: 330px;
63+
min-height: 410px;
6464
padding: 20px 15px;
6565
margin-right: 40px;
6666
line-height: $line-height-base;
@@ -118,6 +118,14 @@
118118
.details {
119119
display: flex;
120120
flex-wrap: wrap;
121+
.resume-details {
122+
font-size: 14px;
123+
line-height: 18px;
124+
& > a {
125+
color: blue;
126+
text-decoration: underline;
127+
}
128+
}
121129

122130
.resume {
123131
align-self: flex-start;

‎src/containers/MyGigs/modals/UpdateGigProfile/tooltips/StatusTooltip/index.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import React from "react";
22
import PT from "prop-types";
33
import Tooltip from "components/Tooltip";
4-
import { gigStatusTooltip } from "assets/data/my-gigs.json";
5-
4+
import { GIG_STATUS_TOOLTIP } from "../../../../../../constants";
5+
import { keys } from "lodash";
66
import "./styles.scss";
77

88
const StatusTooltip = ({ children }) => {
99
const Content = () => (
1010
<div styleName="status-tooltip">
1111
<ul>
12-
{Object.keys(gigStatusTooltip).map((status) => (
12+
{keys(GIG_STATUS_TOOLTIP).map((status) => (
1313
<li styleName="item" key={status}>
1414
<div>
1515
<div styleName="caption">{status}</div>
16-
<div styleName="text">{gigStatusTooltip[status]}</div>
16+
<div styleName="text">{GIG_STATUS_TOOLTIP[status]}</div>
1717
</div>
1818
</li>
1919
))}

‎src/reducers/lookup.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const defaultState = {
88
tags: [],
99
subCommunities: [],
1010
isLoggedIn: null,
11-
gigStatuses: [],
11+
countries: [],
1212
};
1313

1414
function onGetTagsDone(state, { payload }) {
@@ -23,16 +23,16 @@ function onCheckIsLoggedInDone(state, { payload }) {
2323
return { ...state, isLoggedIn: payload };
2424
}
2525

26-
function onGetGigStatusesDone(state, { payload }) {
27-
return { ...state, gigStatuses: payload };
26+
function onGetAllCountriesDone(state, { payload }) {
27+
return { ...state, countries: payload };
2828
}
2929

3030
export default handleActions(
3131
{
3232
GET_TAGS_DONE: onGetTagsDone,
3333
GET_COMMUNITY_LIST_DONE: onGetCommunityListDone,
3434
CHECK_IS_LOGGED_IN_DONE: onCheckIsLoggedInDone,
35-
GET_GIG_STATUSES_DONE: onGetGigStatusesDone,
35+
GET_ALL_COUNTRIES_DONE: onGetAllCountriesDone,
3636
},
3737
defaultState
3838
);

‎src/services/api.js

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { getAuthUserTokens } from "@topcoder/micro-frontends-navbar-app";
2+
import { keys } from "lodash";
23
import * as utils from "../utils";
34

45
async function doFetch(endpoint, options = {}, v3, baseUrl) {
@@ -17,31 +18,35 @@ async function doFetch(endpoint, options = {}, v3, baseUrl) {
1718
headers.Authorization = `Bearer ${token.tokenV3}`;
1819
}
1920

20-
if (!headers["Content-Type"]) {
21-
headers["Content-Type"] = "application/json";
22-
}
23-
2421
return fetch(`${url}${endpoint}`, {
2522
...options,
2623
headers,
2724
});
2825
}
2926

3027
async function get(endpoint, baseUrl) {
31-
const response = await doFetch(endpoint, undefined, undefined, baseUrl);
28+
const options = { headers: { ["Content-Type"]: "application/json" } };
29+
const response = await doFetch(endpoint, options, undefined, baseUrl);
3230
const meta = utils.pagination.getResponseHeaders(response);
3331
const result = await response.json();
34-
result.meta = meta;
32+
// only add pagination info if any field is filled
33+
if (keys(meta).some((key) => meta[key] !== 0)) result.meta = meta;
3534

3635
return result;
3736
}
3837

39-
async function post(endpoint, body) {
40-
const response = await doFetch(endpoint, {
41-
body,
42-
method: "POST",
43-
});
44-
return response.json();
38+
async function post(endpoint, body, baseUrl) {
39+
const response = await doFetch(
40+
endpoint,
41+
{
42+
body,
43+
method: "POST",
44+
},
45+
undefined,
46+
baseUrl
47+
);
48+
// not all responses are json (example http code: 204), so returning just the response.
49+
return response;
4550
}
4651

4752
async function put(endpoint, body) {

‎src/services/lookup.js

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import api from "./api";
22
import qs from "qs";
33
import * as utils from "../utils";
4-
import myGigsData from "../assets/data/my-gigs.json";
54

65
async function getTags() {
76
const v3 = true;
@@ -54,13 +53,20 @@ async function getCommunityList() {
5453
);
5554
}
5655

57-
async function getGigStatuses() {
58-
return Promise.resolve(myGigsData.gigStatuses);
56+
/**
57+
* Gets paginated countries
58+
* @param {number} page page to fetch
59+
* @param {number} perPage number of items by page
60+
* @returns
61+
*/
62+
async function getPaginatedCountries(page = 1, perPage = 100) {
63+
const url = `/lookups/countries?page=${page}&perPage=${perPage}`;
64+
return await api.get(url);
5965
}
6066

6167
export default {
6268
getTags,
6369
getCommunityList,
6470
checkIsLoggedIn,
65-
getGigStatuses,
71+
getPaginatedCountries,
6672
};

‎src/services/myGigs.js

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import api from "./api";
21
import { get, keys, size, sortBy, values } from "lodash";
32
import {
43
ACTIONS_AVAILABLE_FOR_MY_GIG_PHASE,
@@ -8,9 +7,11 @@ import {
87
SORT_STATUS_ORDER,
98
PHASES_FOR_JOB_STATUS,
109
MY_GIG_PHASE,
10+
GIG_STATUS,
1111
} from "../constants";
12-
import data from "../assets/data/my-gigs.json";
12+
import api from "./api";
1313

14+
const PROFILE_URL = "/earn-app/api/my-gigs/profile";
1415
/**
1516
* Maps the data from api to data to be used by application
1617
* @param {Object} serverResponse data returned by the api
@@ -93,12 +94,67 @@ async function getMyGigs(page, perPage) {
9394
};
9495
}
9596

97+
/**
98+
* Get the profile info
99+
* @returns {Object}
100+
*/
96101
async function getProfile() {
97-
return Promise.resolve(data.gigProfile);
102+
const profile = await api.get(
103+
PROFILE_URL,
104+
process.env.URL.PLATFORM_WEBSITE_URL
105+
);
106+
107+
return {
108+
handle: profile.handle,
109+
photoURL: profile.profilePhoto,
110+
firstName: profile.firstName,
111+
lastName: profile.lastName,
112+
email: profile.email,
113+
city: profile.city,
114+
country: profile.country,
115+
phone: profile.phone,
116+
file: null,
117+
existingResume: profile.resume,
118+
hasProfile: profile.hasProfile,
119+
status: profile.availability
120+
? GIG_STATUS.AVAILABLE
121+
: GIG_STATUS.UNAVAILABLE,
122+
};
98123
}
99124

125+
/**
126+
* Updates the profile
127+
* @param {Object} profile - profile info to be updated
128+
* @returns
129+
*/
100130
async function updateProfile(profile) {
101-
return Promise.resolve(profile);
131+
const payload = {
132+
city: profile.city,
133+
country: profile.country,
134+
countryName: profile.countryName,
135+
phone: profile.phone,
136+
availability: profile.status === GIG_STATUS.AVAILABLE ? true : false,
137+
};
138+
if (profile.file) {
139+
payload.resume = profile.file;
140+
}
141+
142+
// add info to formData to send to server
143+
const formData = new FormData();
144+
keys(payload).forEach((key) => formData.append(key, payload[key]));
145+
146+
const response = await api.post(
147+
PROFILE_URL,
148+
formData,
149+
process.env.URL.PLATFORM_WEBSITE_URL
150+
);
151+
152+
// in case of error, throw the server error
153+
if (response.status !== 200 && response.status !== 204) {
154+
const error = await response.json();
155+
throw new Error(error.message);
156+
}
157+
return response;
102158
}
103159

104160
export default {

‎src/utils/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ export * as myGig from "./myGig";
1010
export function createDropdownOptions(values, selectedValue) {
1111
return values.map((value) => ({
1212
label: `${value}`,
13-
selected: !!selectedValue && selectedValue === value,
13+
selected:
14+
!!selectedValue &&
15+
(selectedValue === value ||
16+
(selectedValue.name && selectedValue.name === value)),
1417
}));
1518
}
1619

‎src/utils/myGig.js

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,19 @@ export function validateCity(value) {
5353
}
5454

5555
export function validatePhone(phoneNumber, country) {
56-
const countryCode = countries.getAlpha2Code(country, "en") || "US";
5756
let error = validateTextRequired(phoneNumber);
5857
if (error) {
5958
return error;
6059
}
6160

6261
phoneNumber = phoneNumber.trim();
6362

64-
const code = codes.find((i) => i.isoCode2 === countryCode);
65-
const regionCode = `+${code.countryCodes[0]}`;
66-
67-
error = !phoneNumber.startsWith(regionCode) && "Invalid country code";
68-
63+
const code = codes.find((i) => i.isoCode3 === country);
64+
let regionCode = "";
65+
if (code) {
66+
regionCode = `+${code.countryCodes[0]}`;
67+
error = !phoneNumber.startsWith(regionCode) && "Invalid country code";
68+
}
6969
if (!error) {
7070
const regexValidCharacters = /[\s0-9+-\.()]/g;
7171
error =

‎webpack.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ module.exports = (webpackConfigEnv) => {
124124
// Register routes
125125
require("./src/api/app-routes")(app);
126126
},
127+
hot: true,
127128
port: 8008,
128129
host: "0.0.0.0",
129130
},

0 commit comments

Comments
 (0)
This repository has been archived.