diff --git a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Languages/index.jsx b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Languages/index.jsx
index 4721e00de2..b7d47b7bc6 100644
--- a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Languages/index.jsx
+++ b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Languages/index.jsx
@@ -357,7 +357,7 @@ export default class Languages extends ConsentComponent {
- Language Skills
+ Spoken Languages
{
languageItems.length > 0
@@ -387,7 +387,7 @@ export default class Languages extends ConsentComponent {
valueKey="name"
clearable={false}
/>
- { isSubmit && formInvalid && (
+ {isSubmit && formInvalid && (
)}
- { isEdit && (
+ {isEdit && (
arr && _.findIndex(arr, e => e.toLowerCase() === i.toLowerCase()) !== -1;
const findSkill = (arr, skill) => arr && arr.find(a => a.id === skill.id);
- const popularSkills = React.useMemo(() => allSkills
- .filter(skill => find(skill.categories, category))
- .slice(0, 10)
- .map(skill => _.cloneDeep(({ ...skill, isPopularSkill: true }))),
- [allSkills, category]);
-
const handleSkillSelect = (skill) => {
setEditingSkills([...editingSkills, skill]);
};
@@ -66,15 +59,15 @@ export default function AddSkillsModal({
};
const allDisplayingSkills = displayingSkills;
- popularSkills.forEach((skill) => {
- if (!findSkill(displayingSkills, skill)) {
- allDisplayingSkills.push(skill);
- }
- });
const lookupSkillsOptions = lookupSkills
.filter(skill => !findSkill(allDisplayingSkills, skill))
- .filter(skill => find(skill.categories, category));
+ .filter(skill => find(skill.categories, category))
+ .sort((a, b) => {
+ if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
+ if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
+ return 0;
+ });
const skillList = allDisplayingSkills.map((skill) => {
const isOtherCategorySkill = s => !find(s.categories, category);
@@ -89,7 +82,7 @@ export default function AddSkillsModal({
role="button"
onClick={() => !selected && toggleSkillSelection(skill)}
tabIndex={0}
- onKeyDown={() => {}}
+ onKeyDown={() => { }}
>
{_.truncate(skill.name, { length: 18, separator: ' ' })}
@@ -98,7 +91,7 @@ export default function AddSkillsModal({
onClick={() => selected && toggleSkillSelection(skill)}
styleName="close"
tabIndex={0}
- onKeyDown={() => {}}
+ onKeyDown={() => { }}
>
@@ -131,33 +124,33 @@ export default function AddSkillsModal({
styleName={tab === CATEGORIES.design ? 'active' : ''}
role="presentation"
onClick={() => setTab(CATEGORIES.design)}
- onKeyDown={() => {}}
+ onKeyDown={() => { }}
>
- { getTabName(CATEGORIES.design) }
+ {getTabName(CATEGORIES.design)}
setTab(CATEGORIES.develop)}
- onKeyDown={() => {}}
+ onKeyDown={() => { }}
>
- { getTabName(CATEGORIES.develop) }
+ {getTabName(CATEGORIES.develop)}
setTab(CATEGORIES.data_science)}
- onKeyDown={() => {}}
+ onKeyDown={() => { }}
>
- { getTabName(CATEGORIES.data_science) }
+ {getTabName(CATEGORIES.data_science)}
setTab(CATEGORIES.qa)}
- onKeyDown={() => {}}
+ onKeyDown={() => { }}
>
- { getTabName(CATEGORIES.qa) }
+ {getTabName(CATEGORIES.qa)}
@@ -200,7 +193,6 @@ export default function AddSkillsModal({
}
AddSkillsModal.propTypes = {
- allSkills: PT.arrayOf(PT.shape()).isRequired,
disabled: PT.bool.isRequired,
category: PT.string.isRequired,
editingSkills: PT.arrayOf(PT.shape()).isRequired,
diff --git a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/AddSkillsModal/styles.scss b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/AddSkillsModal/styles.scss
index 40820c3b49..a748173fa8 100755
--- a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/AddSkillsModal/styles.scss
+++ b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/AddSkillsModal/styles.scss
@@ -46,6 +46,7 @@
margin: 32px;
width: calc(100% - 64px);
min-height: 700px;
+ min-width: 40vw;
}
.modal-header {
diff --git a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/index.jsx b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/index.jsx
index b9f825f0a2..842bd30c4f 100755
--- a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/index.jsx
+++ b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/index.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable max-len */
/**
* Child component of Settings/Profile renders "Skills" section of profile setting page.
*/
@@ -13,11 +14,12 @@ import ConfirmationModal from 'components/Settings/ConfirmationModal';
import AddItemIcon from 'assets/images/settings-add-item.svg';
import RemoveTagIcon from 'assets/images/icon-x-cancel.svg';
import { SettingBannerV2 as Collapse } from 'components/Settings/SettingsBanner';
-import AddSkillsModal from './AddSkillsModal';
+import { MIN_SKILLS_TO_REMIND } from 'containers/Gamification';
+import YouGotSkillsBadge from 'components/Gamification/YouGotSkillsModal/YouGotSkillsBadge';
+import AddSkillsModal from './AddSkillsModal';
import styles from './styles.scss';
-
export default class Skills extends ConsentComponent {
constructor(props) {
super(props);
@@ -317,7 +319,7 @@ export default class Skills extends ConsentComponent {
{
_.map(list, skill => (
- {_.includes(skill.sources, 'CHALLENGE') && }
+ {_.includes(skill.sources, 'CHALLENGE') && }
{_.truncate(skill.name, { length: 18, separator: ' ' })}
{}}
+ onKeyDown={() => { }}
>
@@ -356,9 +358,8 @@ export default class Skills extends ConsentComponent {
/>
)
}
- { showAddSkillsModal && (
+ {showAddSkillsModal && (
-
- Add your skills
-
+ = MIN_SKILLS_TO_REMIND ? 'center' : 'flex-start' }}>
+
Skills
+ {
+ userSkills.length >= MIN_SKILLS_TO_REMIND &&
+ }
+
@@ -382,6 +386,27 @@ export default class Skills extends ConsentComponent {
+ {
+ userSkills.length < MIN_SKILLS_TO_REMIND && (
+
+
+
+
+
To be able to match you with the best opportunities at Topcoder, please be sure you have at least {MIN_SKILLS_TO_REMIND} skills listed in your profile.
+
+ )
+ }
{skillList}
@@ -391,31 +416,7 @@ export default class Skills extends ConsentComponent {
disabled={!canModifyTrait}
theme={{ button: styles['button-add'] }}
>
-
Add Design / UX Skills
-
-
-
this.setState({ showAddSkillsModal: 'develop' })}
- disabled={!canModifyTrait}
- theme={{ button: styles['button-add'] }}
- >
- Add Developer Skills
-
-
-
this.setState({ showAddSkillsModal: 'data_science' })}
- disabled={!canModifyTrait}
- theme={{ button: styles['button-add'] }}
- >
- Add Data Science Skills
-
-
-
this.setState({ showAddSkillsModal: 'qa' })}
- disabled={!canModifyTrait}
- theme={{ button: styles['button-add'] }}
- >
- Add QA Skills
+ Add Skills
diff --git a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/styles.scss b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/styles.scss
index f1482d4522..afd9ae5234 100644
--- a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/styles.scss
+++ b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Skills/styles.scss
@@ -1,3 +1,4 @@
+/* stylelint-disable no-descending-specificity */
@import "~styles/mixins";
.skillList {
@@ -47,10 +48,21 @@
.form-container {
padding: $pad-xxxxl;
- margin: $margin-sm 0 0;
+ margin: $margin-sm 0;
background-color: $color-tc-white;
}
+.title-wrap {
+ display: flex;
+ padding-bottom: 12px;
+
+ p {
+ @include roboto-regular;
+
+ line-height: 26px;
+ }
+}
+
.form-title {
@include barlow-semi-bold;
@@ -58,7 +70,7 @@
line-height: 22px;
color: inherit;
text-transform: uppercase;
- padding-bottom: $pad-xxxxl;
+ margin-right: 32px;
}
.form-content {
@@ -81,6 +93,8 @@
.form-body {
flex: 0 0 calc(50% + 13px);
+ display: flex;
+ flex-direction: column;
}
.form-footer {
@@ -120,17 +134,47 @@
}
}
+.skill-note {
+ display: flex;
+ background-color: $color-black-5;
+ padding: $pad-md $pad-lg;
+ border-radius: 4px;
+ margin-bottom: $pad-lg;
+
+ svg {
+ margin-right: $margin-md;
+ min-width: 24px;
+ }
+
+ p,
+ span {
+ @include roboto-medium;
+
+ line-height: 24px;
+ }
+
+ span {
+ color: $color-turq-160;
+ }
+}
+
@include xs-to-md {
.form-container {
padding: $pad-xxl $pad-lg;
}
+ .title-wrap {
+ p {
+ font-size: 14px;
+ line-height: 24px;
+ }
+ }
+
.form-title {
@include barlow-condensed-medium;
font-size: 22px;
line-height: 24px;
- padding-bottom: $pad-xxl;
}
.form-content {
@@ -162,3 +206,13 @@
}
}
}
+
+@include xs-to-sm {
+ .skill-note {
+ flex-direction: column;
+
+ svg {
+ margin-bottom: $margin-md;
+ }
+ }
+}
diff --git a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/index.jsx b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/index.jsx
index 1290989e85..f5caa0b13e 100644
--- a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/index.jsx
+++ b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/index.jsx
@@ -16,7 +16,11 @@ export default class WorkSkills extends React.Component {
render() {
return (
-
Skills
+ About You
+
-
);
}
diff --git a/src/shared/components/Settings/ExperienceAndSkills/index.jsx b/src/shared/components/Settings/ExperienceAndSkills/index.jsx
index 6dfcd544ee..461ca66cff 100644
--- a/src/shared/components/Settings/ExperienceAndSkills/index.jsx
+++ b/src/shared/components/Settings/ExperienceAndSkills/index.jsx
@@ -94,14 +94,14 @@ export default class ExperienceAndSkills extends React.Component {
return (
@@ -111,7 +111,7 @@ export default class ExperienceAndSkills extends React.Component {
ExperienceAndSkills.defaultProps = {
isSaving: false,
- setIsSaving: () => {},
+ setIsSaving: () => { },
};
ExperienceAndSkills.propTypes = {
diff --git a/src/shared/components/Settings/constants.js b/src/shared/components/Settings/constants.js
index ef912b9e04..821aebf83a 100644
--- a/src/shared/components/Settings/constants.js
+++ b/src/shared/components/Settings/constants.js
@@ -7,7 +7,7 @@ export const SCREEN_SIZE = {
export const SETTINGS_TABS = [
{ title: 'Profile', link: TABS.PROFILE },
- { title: 'Experience & Skills', link: TABS.SKILLS },
+ { title: 'Skills & Experience', link: TABS.SKILLS },
{ title: 'Topcoder & You', link: TABS.TRACKS },
{ title: 'Tools', link: TABS.TOOLS },
{ title: 'Account', link: TABS.ACCOUNT },
diff --git a/src/shared/containers/Gamification/index.jsx b/src/shared/containers/Gamification/index.jsx
new file mode 100644
index 0000000000..99858b13fc
--- /dev/null
+++ b/src/shared/containers/Gamification/index.jsx
@@ -0,0 +1,209 @@
+/**
+ * Gamification container
+ */
+/* global analytics */
+/* eslint-disable react/no-did-update-set-state */
+import React from 'react';
+import PT from 'prop-types';
+import { connect } from 'react-redux';
+import SkillsNagModal from 'components/Gamification/SkillsNagModal';
+import YouGotSkillsModal from 'components/Gamification/YouGotSkillsModal';
+import { keys } from 'lodash';
+import cookies from 'browser-cookies';
+import { actions } from 'topcoder-react-lib';
+
+const REMIND_TIME_COOKIE_NAME = 'tc_skills_remind_time';
+const REMIND_TIME = 604800000; // a week
+export const MIN_SKILLS_TO_REMIND = 5;
+
+class GamificationContainer extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ showSkillsNagModal: false,
+ showYouGotSkillsModal: false,
+ };
+
+ this.onSkillsNagModalCancel = this.onSkillsNagModalCancel.bind(this);
+ this.onSkillsNagModalCTA = this.onSkillsNagModalCTA.bind(this);
+ this.onYouGotSkillsModalCancel = this.onYouGotSkillsModalCancel.bind(this);
+ this.onYouGotSkillsModalCTA = this.onYouGotSkillsModalCTA.bind(this);
+ }
+
+ componentDidMount() {
+ const { auth, loadSkills } = this.props;
+
+ if (auth.tokenV3 && auth.user.handle) {
+ loadSkills(auth.user.handle);
+ }
+ }
+
+ componentDidUpdate(prevProps) {
+ const { profile: prevProfile } = prevProps;
+ const { profile, auth } = this.props;
+ const { state } = this;
+
+ if (
+ !auth.tokenV3
+ || (profile.profileForHandle && profile.profileForHandle !== auth.user.handle)
+ || (prevProfile.profileForHandle && prevProfile.profileForHandle !== auth.user.handle)
+ ) {
+ return;
+ }
+
+ const lastNagTime = state.lastNagTime || cookies.get(`${REMIND_TIME_COOKIE_NAME}_${auth.user.handle}`);
+ const now = new Date().getTime();
+
+ // when to show YouGotSkills modal
+ if (
+ prevProfile
+ && prevProfile.skills !== null
+ && profile.skills !== null
+ && keys(prevProfile.skills).length < MIN_SKILLS_TO_REMIND
+ && keys(profile.skills).length >= MIN_SKILLS_TO_REMIND
+ && state.showYouGotSkillsModal === false
+ ) {
+ this.setState({
+ ...state,
+ showYouGotSkillsModal: true,
+ });
+ }
+
+ // when to show the nag modal
+ if (
+ window.location.pathname !== '/settings/skills'
+ && !state.showSkillsNagModal
+ && (now - lastNagTime) > REMIND_TIME
+ && profile.skills !== null
+ && keys(profile.skills).length < MIN_SKILLS_TO_REMIND
+ ) {
+ this.setState({
+ showSkillsNagModal: true,
+ lastNagTime: `${now}`,
+ });
+ cookies.set(`${REMIND_TIME_COOKIE_NAME}_${auth.user.handle}`, `${now}`, { expires: 7 });
+ }
+ }
+
+ onYouGotSkillsModalCTA() {
+ const { profile, auth } = this.props;
+ const { state } = this;
+ this.setState({
+ ...state,
+ showYouGotSkillsModal: false,
+ });
+ // track the CTA event
+ analytics.track('Member clicked CTA button on YouGotSkills modal', {
+ ...profile,
+ });
+ // Send them to profile page
+ window.location = `/members/${auth.user.handle}`;
+ }
+
+ onYouGotSkillsModalCancel() {
+ const { state } = this;
+ this.setState({
+ ...state,
+ showYouGotSkillsModal: false,
+ });
+ }
+
+ onSkillsNagModalCTA() {
+ const { profile } = this.props;
+ const { state } = this;
+ this.setState({
+ ...state,
+ showSkillsNagModal: false,
+ });
+ // track the CTA event
+ analytics.track('Member clicked CTA button on skills remind modal', {
+ ...profile,
+ });
+ // navigate to skills
+ window.location = '/settings/skills';
+ }
+
+ onSkillsNagModalCancel() {
+ const { profile } = this.props;
+ const { state } = this;
+ this.setState({
+ ...state,
+ showSkillsNagModal: false,
+ });
+ // track the cancel event
+ analytics.track('Member canceled skills remind modal', {
+ ...profile,
+ });
+ }
+
+ render() {
+ const { profile, auth } = this.props;
+ const { state } = this;
+
+ if (!auth.tokenV3) {
+ return null;
+ }
+
+ return (
+
+ {
+ state.showSkillsNagModal && (
+
+ )
+ }
+ {
+ state.showYouGotSkillsModal && (
+
+ )
+ }
+
+ );
+ }
+}
+
+GamificationContainer.defaultProps = {
+ profile: null,
+ auth: {},
+};
+
+GamificationContainer.propTypes = {
+ profile: PT.shape(),
+ auth: PT.shape(),
+ loadSkills: PT.func.isRequired,
+};
+
+function mapDispatchToProps(dispatch) {
+ const profileActions = actions.profile;
+ const lookupActions = actions.lookup;
+
+ return {
+ loadSkills: (handle) => {
+ dispatch(profileActions.getSkillsDone(handle));
+ dispatch(lookupActions.getSkillTagsInit());
+ dispatch(lookupActions.getSkillTagsDone());
+ },
+ };
+}
+
+function mapStateToProps(state) {
+ return {
+ auth: { ...state.auth },
+ profile: state.profile,
+ };
+}
+
+export default connect(
+ mapStateToProps,
+ mapDispatchToProps,
+)(GamificationContainer);