diff --git a/.circleci/config.yml b/.circleci/config.yml index 604c51da97..ee4a76c115 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -370,7 +370,7 @@ workflows: filters: branches: only: - - beta-demo + - GAME-193 # This is stage env for production QA releases - "build-prod-staging": context : org-global diff --git a/__tests__/shared/components/Settings/__snapshots__/index.jsx.snap b/__tests__/shared/components/Settings/__snapshots__/index.jsx.snap index 73f0c5c1d3..ea529b623b 100644 --- a/__tests__/shared/components/Settings/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/Settings/__snapshots__/index.jsx.snap @@ -30,7 +30,7 @@ exports[`renders account setting page correctly 1`] = ` }, Object { "link": "skills", - "title": "Experience & Skills", + "title": "Skills & Experience", }, Object { "link": "tracks", @@ -787,7 +787,7 @@ exports[`renders preferences setting page correctly 1`] = ` }, Object { "link": "skills", - "title": "Experience & Skills", + "title": "Skills & Experience", }, Object { "link": "tracks", @@ -1541,7 +1541,7 @@ exports[`renders profile setting page correctly 1`] = ` }, Object { "link": "skills", - "title": "Experience & Skills", + "title": "Skills & Experience", }, Object { "link": "tracks", @@ -2301,7 +2301,7 @@ exports[`renders tools setting page correctly 1`] = ` }, Object { "link": "skills", - "title": "Experience & Skills", + "title": "Skills & Experience", }, Object { "link": "tracks", diff --git a/config/default.js b/config/default.js index 2a06bb3054..dd82e659e7 100644 --- a/config/default.js +++ b/config/default.js @@ -461,6 +461,7 @@ module.exports = { GAMIFICATION: { ORG_ID: '6052dd9b-ea80-494b-b258-edd1331e27a3', ENABLE_BADGE_UI: true, + ENABLE_SKILLS_REMIND_MODAL: true, }, PLATFORMUI_SITE_URL: 'https://platform-ui.topcoder-dev.com', DICE_VERIFY_URL: 'https://accounts-auth0.topcoder-dev.com', diff --git a/src/shared/app.jsx b/src/shared/app.jsx index 4199a32cf7..9f98dad665 100644 --- a/src/shared/app.jsx +++ b/src/shared/app.jsx @@ -14,6 +14,7 @@ import ErrorIcons from 'containers/ErrorIcons'; import { DevTools, isomorphy, config } from 'topcoder-react-utils'; import ExtendedReduxToastr from 'containers/Toastr'; +import Gamification from 'containers/Gamification'; import 'react-redux-toastr/lib/css/react-redux-toastr.min.css'; @@ -51,7 +52,12 @@ export default function App() { progressBar={false} showCloseButton /> - { isomorphy.isDevBuild() ? : undefined } + {isomorphy.isDevBuild() ? : undefined} + { + config.GAMIFICATION.ENABLE_SKILLS_REMIND_MODAL + && isomorphy.isClientSide() + ? : undefined + } ); } diff --git a/src/shared/components/Gamification/SkillsNagModal/index.jsx b/src/shared/components/Gamification/SkillsNagModal/index.jsx new file mode 100644 index 0000000000..cd3bd1550e --- /dev/null +++ b/src/shared/components/Gamification/SkillsNagModal/index.jsx @@ -0,0 +1,163 @@ +import React from 'react'; +import PT from 'prop-types'; +import { Modal } from 'topcoder-react-ui-kit'; +import { keys } from 'lodash'; + +import IconClose from 'assets/images/icon-close-green.svg'; +import style from './styles.scss'; + +const skillCountStatement = (count) => { + let statement = ''; + switch (count) { + case 0: + statement = 'don’t have any skills'; + break; + case 1: + statement = 'only have 1 skill'; + break; + default: + statement = `only have ${count} skills`; + } + return statement; +}; + +const SkillsNagModal = ({ + handle, + skills, + onCancel, + onCTA, + MIN_SKILLS_TO_REMIND, +}) => ( + +
+ +
+
+ UPDATE PROFILE SKILLS +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + {/* eslint-disable-next-line max-len */} + Hey {handle}, we have noticed that you {skillCountStatement(keys(skills).length)} added to your profile. To be able to match you with the best opportunities at Topcoder, please add skills so that you have at least {MIN_SKILLS_TO_REMIND} skills listed in your profile. + +
+ +
+ +
+ +
+
+); + +SkillsNagModal.propTypes = { + MIN_SKILLS_TO_REMIND: PT.number.isRequired, + handle: PT.string.isRequired, + skills: PT.shape().isRequired, + onCancel: PT.func.isRequired, + onCTA: PT.func.isRequired, +}; + +export default SkillsNagModal; diff --git a/src/shared/components/Gamification/SkillsNagModal/styles.scss b/src/shared/components/Gamification/SkillsNagModal/styles.scss new file mode 100644 index 0000000000..3d1cde723d --- /dev/null +++ b/src/shared/components/Gamification/SkillsNagModal/styles.scss @@ -0,0 +1,118 @@ +@import "~styles/mixins"; +@import "~components/Contentful/brackets"; + +.nagModal { + display: flex; + flex-direction: column; + margin: 32px; + + @include xs-to-md { + flex-direction: column; + margin-top: 24px; + } + + .header { + display: flex; + align-items: flex-start; + justify-content: space-between; + border-bottom: 2px solid #e9e9e9; + + @include xs-to-md { + margin-top: 24px; + text-align: center; + } + + .title { + @include barlow-bold; + + color: $tco-black; + font-size: 22px; + font-weight: 600; + line-height: 26px; + margin-bottom: 12px; + text-transform: uppercase; + + @include xs-to-md { + text-align: center; + } + } + + .icon { + cursor: pointer; + } + } + + .description { + @include roboto-regular; + + font-weight: 400; + color: $tco-black; + font-size: 16px; + line-height: 24px; + margin-top: 24px; + + strong { + font-weight: 700; + } + + .badgeWrap { + display: flex; + justify-content: center; + margin-bottom: 12px; + } + + span span { + color: #137d60; + font-weight: bold; + } + } +} + +.container { + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + border-radius: 8px; + min-width: 600px; + + @include xs-to-sm { + width: 90%; + min-width: unset; + } +} + +.overlay { + background-color: #0c0c0c; + opacity: 0.85; +} + +.actionButtons { + display: flex; + align-items: center; + justify-content: flex-end; + margin-top: 32px; + padding-top: 24px; + border-top: 2px solid #e9e9e9; + + .primaryBtn { + background-color: #137d60; + border-radius: 24px; + color: #fff; + font-size: 13px; + font-weight: bolder; + text-decoration: none; + text-transform: uppercase; + line-height: 32px; + padding: 0 20px; + border: none; + outline: none; + display: flex; + + &:hover { + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2); + background-color: #0ab88a; + } + + @include xs-to-sm { + margin-bottom: 20px; + } + } +} diff --git a/src/shared/components/Gamification/YouGotSkillsModal/YouGotSkillsBadge.jsx b/src/shared/components/Gamification/YouGotSkillsModal/YouGotSkillsBadge.jsx new file mode 100644 index 0000000000..eb68c710e0 --- /dev/null +++ b/src/shared/components/Gamification/YouGotSkillsModal/YouGotSkillsBadge.jsx @@ -0,0 +1,106 @@ +import React from 'react'; +import { MIN_SKILLS_TO_REMIND } from 'containers/Gamification'; + +export default function YouGotSkillsBadge() { + return ( + + {`You’ve added at least ${MIN_SKILLS_TO_REMIND} skills to your profile!`} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/shared/components/Gamification/YouGotSkillsModal/index.jsx b/src/shared/components/Gamification/YouGotSkillsModal/index.jsx new file mode 100644 index 0000000000..255ddc063c --- /dev/null +++ b/src/shared/components/Gamification/YouGotSkillsModal/index.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PT from 'prop-types'; +import { Modal } from 'topcoder-react-ui-kit'; + +import IconClose from 'assets/images/icon-close-green.svg'; +import YouGotSkillsBadge from './YouGotSkillsBadge'; +import style from './styles.scss'; + +const YouGotSkillsModal = ({ + MIN_SKILLS_TO_REMIND, + onCancel, + onCTA, +}) => ( + +
+ +
+
+ YOU’VE GOT SKILLS! +
+
+ +
+
+ +
+
+ +
+ + {/* eslint-disable-next-line max-len */} + By having {MIN_SKILLS_TO_REMIND} or more skills associated with your profile, not only will you have a more curated experience here at Topcoder, but you will also be presented with more opportunities that align with your strengths and interests. + +
+ +
+ +
+ +
+
+); + +YouGotSkillsModal.propTypes = { + MIN_SKILLS_TO_REMIND: PT.number.isRequired, + onCancel: PT.func.isRequired, + onCTA: PT.func.isRequired, +}; + +export default YouGotSkillsModal; diff --git a/src/shared/components/Gamification/YouGotSkillsModal/styles.scss b/src/shared/components/Gamification/YouGotSkillsModal/styles.scss new file mode 100644 index 0000000000..d5f1a97568 --- /dev/null +++ b/src/shared/components/Gamification/YouGotSkillsModal/styles.scss @@ -0,0 +1,113 @@ +@import "~styles/mixins"; +@import "~components/Contentful/brackets"; + +.gotSkillsModal { + display: flex; + flex-direction: column; + margin: 32px; + + @include xs-to-md { + flex-direction: column; + margin-top: 24px; + } + + .header { + display: flex; + align-items: flex-start; + justify-content: space-between; + border-bottom: 2px solid #e9e9e9; + + @include xs-to-md { + margin-top: 24px; + text-align: center; + } + + .title { + @include barlow-bold; + + color: $tco-black; + font-size: 20px; + font-weight: 700; + line-height: 26px; + margin-bottom: 12px; + text-transform: uppercase; + + @include xs-to-md { + text-align: center; + } + } + + .icon { + cursor: pointer; + } + } + + .description { + @include roboto-regular; + + font-weight: 400; + color: $tco-black; + font-size: 16px; + line-height: 24px; + margin-top: 24px; + + strong { + font-weight: 700; + } + + .badgeWrap { + display: flex; + justify-content: center; + margin-bottom: 12px; + } + } +} + +.container { + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.2); + border-radius: 8px; + min-width: 600px; + + @include xs-to-sm { + width: 90%; + min-width: unset; + } +} + +.overlay { + background-color: #0c0c0c; + opacity: 0.85; +} + +.actionButtons { + display: flex; + align-items: center; + justify-content: flex-end; + margin-top: 32px; + padding-top: 24px; + border-top: 2px solid #e9e9e9; + + .primaryBtn { + background-color: #137d60; + border-radius: 24px; + color: #fff; + font-size: 13px; + font-weight: bolder; + text-decoration: none; + text-transform: uppercase; + line-height: 32px; + padding: 0 20px; + border: none; + outline: none; + display: flex; + + &:hover { + box-shadow: 0 1px 5px 0 rgba(0, 0, 0, 0.2); + background-color: #0ab88a; + } + + @include xs-to-sm { + margin-bottom: 20px; + } + } +} diff --git a/src/shared/components/Settings/ExperienceAndSkills/WorkExperience/index.jsx b/src/shared/components/Settings/ExperienceAndSkills/WorkExperience/index.jsx index 4a1396d56b..5650321910 100644 --- a/src/shared/components/Settings/ExperienceAndSkills/WorkExperience/index.jsx +++ b/src/shared/components/Settings/ExperienceAndSkills/WorkExperience/index.jsx @@ -431,7 +431,7 @@ export default class Work extends ConsentComponent {
{ isEdit ? (Edit workplace) - : (Add a new workplace) + : (Jobs) }
{ @@ -567,7 +567,7 @@ export default class Work extends ConsentComponent { theme={{ button: styles.button }} onClick={this.onHandleAddWork} > - Add Another Job + Add Job )} diff --git a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Interests/index.jsx b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Interests/index.jsx index f90fdb97e6..aefad8df48 100755 --- a/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Interests/index.jsx +++ b/src/shared/components/Settings/ExperienceAndSkills/WorkSkills/Interests/index.jsx @@ -201,7 +201,7 @@ export default class Interests extends ConsentComponent {
- Select Your Interests at Topcoder + Interests at Topcoder
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 (
    - +
    {saveBtn}
    @@ -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);