From 0e6136cb66e2ace53284886474abde5fdbb5334b Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Sun, 23 Feb 2020 18:44:22 +0800 Subject: [PATCH 1/8] output from challenge 30115867 --- .../components/__snapshots__/Content.jsx.snap | 29 + package.json | 3 + src/shared/components/Content/index.jsx | 17 + .../MemberSearch/EndOfResults/index.jsx | 18 + .../MemberSearch/EndOfResults/style.scss | 8 + .../MemberSearch/ListContainer/index.jsx | 52 ++ .../MemberSearch/ListContainer/style.scss | 47 ++ .../MemberSearch/LoadMoreButton/index.jsx | 22 + .../MemberSearch/LoadMoreButton/style.scss | 15 + .../MemberSearch/LoadingListItem/index.jsx | 50 ++ .../MemberSearch/LoadingListItem/style.scss | 133 ++++ .../MemberItem/UserInfo/index.jsx | 62 ++ .../MemberItem/UserInfo/style.scss | 32 + .../MemberItem/UserStats/index.jsx | 56 ++ .../MemberItem/UserStats/style.scss | 28 + .../MemberSearch/MemberItem/index.jsx | 57 ++ .../MemberSearch/MemberItem/style.scss | 31 + .../MemberSearch/MemberList/index.jsx | 19 + .../MemberSearch/MemberList/style.scss | 10 + .../MemberSearch/MemberSearchView/index.jsx | 226 ++++++ .../MemberSearch/MemberSearchView/style.scss | 10 + .../MemberSearch/NoResults/index.jsx | 20 + .../MemberSearch/NoResults/style.scss | 32 + .../MemberSearch/PageError/index.jsx | 15 + .../MemberSearch/PageError/style.scss | 27 + .../SubtrackList/SubtrackItem/index.jsx | 53 ++ .../SubtrackList/SubtrackItem/style.scss | 52 ++ .../MemberSearch/SubtrackList/index.jsx | 17 + .../MemberSearch/SubtrackList/style.scss | 12 + .../MemberSearch/TagList/TagItem/index.jsx | 29 + .../MemberSearch/TagList/TagItem/style.scss | 37 + .../components/MemberSearch/TagList/index.jsx | 43 + .../MemberSearch/TagList/style.scss | 29 + .../MemberSearch/TopMemberList/index.jsx | 24 + .../TrackList/TrackItem/index.jsx | 33 + .../TrackList/TrackItem/style.scss | 46 ++ .../MemberSearch/TrackList/index.jsx | 26 + .../MemberSearch/TrackList/style.scss | 12 + .../User/UserAvatar/default-avatar.svg | 20 + .../MemberSearch/User/UserAvatar/index.jsx | 43 + .../MemberSearch/User/UserAvatar/style.scss | 23 + .../MemberSearch/User/UserBio/index.jsx | 20 + .../MemberSearch/User/UserBio/style.scss | 9 + .../User/UsernameAndDetails/index.jsx | 51 ++ .../User/UsernameAndDetails/style.scss | 36 + .../MemberSearch/helpers/ISOCountries.js | 751 ++++++++++++++++++ .../components/MemberSearch/helpers/index.js | 246 ++++++ .../icons/LevelDesignatorIcon.jsx | 33 + .../MemberSearch/icons/RobotIcon.jsx | 38 + .../MemberSearch/icons/TrophyIcon.jsx | 22 + src/shared/components/MemberSearch/index.jsx | 70 ++ src/shared/components/MemberSearch/style.scss | 43 + src/shared/containers/MemberSearch.jsx | 85 ++ src/shared/routes/Topcoder/Routes.jsx | 6 + 54 files changed, 2928 insertions(+) create mode 100644 src/shared/components/MemberSearch/EndOfResults/index.jsx create mode 100644 src/shared/components/MemberSearch/EndOfResults/style.scss create mode 100644 src/shared/components/MemberSearch/ListContainer/index.jsx create mode 100644 src/shared/components/MemberSearch/ListContainer/style.scss create mode 100644 src/shared/components/MemberSearch/LoadMoreButton/index.jsx create mode 100644 src/shared/components/MemberSearch/LoadMoreButton/style.scss create mode 100644 src/shared/components/MemberSearch/LoadingListItem/index.jsx create mode 100644 src/shared/components/MemberSearch/LoadingListItem/style.scss create mode 100644 src/shared/components/MemberSearch/MemberItem/UserInfo/index.jsx create mode 100644 src/shared/components/MemberSearch/MemberItem/UserInfo/style.scss create mode 100644 src/shared/components/MemberSearch/MemberItem/UserStats/index.jsx create mode 100644 src/shared/components/MemberSearch/MemberItem/UserStats/style.scss create mode 100644 src/shared/components/MemberSearch/MemberItem/index.jsx create mode 100644 src/shared/components/MemberSearch/MemberItem/style.scss create mode 100644 src/shared/components/MemberSearch/MemberList/index.jsx create mode 100644 src/shared/components/MemberSearch/MemberList/style.scss create mode 100644 src/shared/components/MemberSearch/MemberSearchView/index.jsx create mode 100644 src/shared/components/MemberSearch/MemberSearchView/style.scss create mode 100644 src/shared/components/MemberSearch/NoResults/index.jsx create mode 100644 src/shared/components/MemberSearch/NoResults/style.scss create mode 100644 src/shared/components/MemberSearch/PageError/index.jsx create mode 100644 src/shared/components/MemberSearch/PageError/style.scss create mode 100644 src/shared/components/MemberSearch/SubtrackList/SubtrackItem/index.jsx create mode 100644 src/shared/components/MemberSearch/SubtrackList/SubtrackItem/style.scss create mode 100644 src/shared/components/MemberSearch/SubtrackList/index.jsx create mode 100644 src/shared/components/MemberSearch/SubtrackList/style.scss create mode 100644 src/shared/components/MemberSearch/TagList/TagItem/index.jsx create mode 100644 src/shared/components/MemberSearch/TagList/TagItem/style.scss create mode 100644 src/shared/components/MemberSearch/TagList/index.jsx create mode 100644 src/shared/components/MemberSearch/TagList/style.scss create mode 100644 src/shared/components/MemberSearch/TopMemberList/index.jsx create mode 100644 src/shared/components/MemberSearch/TrackList/TrackItem/index.jsx create mode 100644 src/shared/components/MemberSearch/TrackList/TrackItem/style.scss create mode 100644 src/shared/components/MemberSearch/TrackList/index.jsx create mode 100644 src/shared/components/MemberSearch/TrackList/style.scss create mode 100644 src/shared/components/MemberSearch/User/UserAvatar/default-avatar.svg create mode 100644 src/shared/components/MemberSearch/User/UserAvatar/index.jsx create mode 100644 src/shared/components/MemberSearch/User/UserAvatar/style.scss create mode 100644 src/shared/components/MemberSearch/User/UserBio/index.jsx create mode 100644 src/shared/components/MemberSearch/User/UserBio/style.scss create mode 100644 src/shared/components/MemberSearch/User/UsernameAndDetails/index.jsx create mode 100644 src/shared/components/MemberSearch/User/UsernameAndDetails/style.scss create mode 100644 src/shared/components/MemberSearch/helpers/ISOCountries.js create mode 100644 src/shared/components/MemberSearch/helpers/index.js create mode 100644 src/shared/components/MemberSearch/icons/LevelDesignatorIcon.jsx create mode 100644 src/shared/components/MemberSearch/icons/RobotIcon.jsx create mode 100644 src/shared/components/MemberSearch/icons/TrophyIcon.jsx create mode 100644 src/shared/components/MemberSearch/index.jsx create mode 100644 src/shared/components/MemberSearch/style.scss create mode 100644 src/shared/containers/MemberSearch.jsx diff --git a/__tests__/shared/components/__snapshots__/Content.jsx.snap b/__tests__/shared/components/__snapshots__/Content.jsx.snap index 9af1ad4f52..81e4b665cb 100644 --- a/__tests__/shared/components/__snapshots__/Content.jsx.snap +++ b/__tests__/shared/components/__snapshots__/Content.jsx.snap @@ -325,6 +325,35 @@ exports[`Matches shallow shapshot 1`] = ` Track Home Page - Development +
  • + + Member Search 1 + + , + + Member Search 2 + + , + + Member Search 3 + + , + + Member Search 4 + +
  • TCO Assets diff --git a/package.json b/package.json index 9c54d9d29f..76c1afea31 100644 --- a/package.json +++ b/package.json @@ -86,12 +86,14 @@ "raf": "^3.4.0", "rc-tooltip": "^3.4.9", "react": "^16.4.1", + "react-addons-css-transition-group": "^15.6.2", "react-anchor-link-smooth-scroll": "^1.0.11", "react-color": "^2.13.8", "react-css-super-themr": "^2.2.0", "react-custom-scrollbars": "^4.2.1", "react-dates": "^18.2.2", "react-dom": "^16.4.1", + "react-dotdotdot": "^1.3.1", "react-helmet": "^5.2.0", "react-html-parser": "^2.0.2", "react-image-fallback": "^7.1.0", @@ -124,6 +126,7 @@ "supertest": "^3.1.0", "tc-accounts": "git+https://github.com/appirio-tech/accounts-app.git#dev", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", + "tc-ui": "^1.0.12", "topcoder-react-lib": "1000.7.0", "topcoder-react-ui-kit": "^1.0.11", "topcoder-react-utils": "0.7.8", diff --git a/src/shared/components/Content/index.jsx b/src/shared/components/Content/index.jsx index 39369b6e1c..e32477f91c 100644 --- a/src/shared/components/Content/index.jsx +++ b/src/shared/components/Content/index.jsx @@ -297,6 +297,23 @@ export default function Content() { Track Home Page - Development +
  • + + Member Search 1 + + {', '} + + Member Search 2 + + {', '} + + Member Search 3 + + {', '} + + Member Search 4 + +
  • diff --git a/src/shared/components/MemberSearch/EndOfResults/index.jsx b/src/shared/components/MemberSearch/EndOfResults/index.jsx new file mode 100644 index 0000000000..f4369ea2ba --- /dev/null +++ b/src/shared/components/MemberSearch/EndOfResults/index.jsx @@ -0,0 +1,18 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './style.scss'; + +const EndOfResults = ({ endOfResultsText }) => ( +
    {endOfResultsText}
    +); + +EndOfResults.propTypes = { + endOfResultsText: PropTypes.string, +}; + +EndOfResults.defaultProps = { + endOfResultsText: 'End of results', +}; + +export default EndOfResults; diff --git a/src/shared/components/MemberSearch/EndOfResults/style.scss b/src/shared/components/MemberSearch/EndOfResults/style.scss new file mode 100644 index 0000000000..f3f49d2a9e --- /dev/null +++ b/src/shared/components/MemberSearch/EndOfResults/style.scss @@ -0,0 +1,8 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.end-of-results { + margin: 20px auto; + text-align: center; + font-size: 12px; + color: $tc-gray-50; +} diff --git a/src/shared/components/MemberSearch/ListContainer/index.jsx b/src/shared/components/MemberSearch/ListContainer/index.jsx new file mode 100644 index 0000000000..90e452721c --- /dev/null +++ b/src/shared/components/MemberSearch/ListContainer/index.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { singlePluralFormatter } from '../helpers'; + +import './style.scss'; + +const ListContainer = ({ + headerText, + headerHighlightedText, + children, + numListItems, +}) => { + function renderListCount(numItems) { + if (numItems) { + const listCountMessage = singlePluralFormatter(numItems, 'result'); + + return {` - ${listCountMessage}`}; + } + + return null; + } + + const listCount = renderListCount(numListItems); + + return ( +
    +
    + {headerText} + {headerHighlightedText} + + + {listCount} +
    + + {children} +
    + ); +}; + +ListContainer.propTypes = { + headerText: PropTypes.string.isRequired, + headerHighlightedText: PropTypes.string, + children: PropTypes.shape({}).isRequired, + numListItems: PropTypes.number, +}; + +ListContainer.defaultProps = { + headerHighlightedText: '', + numListItems: [], +}; + +export default ListContainer; diff --git a/src/shared/components/MemberSearch/ListContainer/style.scss b/src/shared/components/MemberSearch/ListContainer/style.scss new file mode 100644 index 0000000000..2c3be47f2c --- /dev/null +++ b/src/shared/components/MemberSearch/ListContainer/style.scss @@ -0,0 +1,47 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.list-container { + max-width: 960px; + margin: 20px auto; + border: 1px solid $tc-gray-30; + box-shadow: 0 1px 2px 0 $tc-gray-30; +} + +.list-header { + padding: 11px 15px; + background-color: $tc-gray-20; + + @include tc-label; + + .header-text { + color: $tc-gray-70; + } + + .highlighted { + font-weight: bold; + } + + .list-count { + color: $tc-gray-70; + opacity: 0.5; + } +} + +// ReactCSSTransitionGroup transitions +.list-container-appear { + opacity: 0.01; + + &.list-container-appear-active { + opacity: 1; + transition: opacity 0.25s ease-in; + } +} + +.list-container-leave { + opacity: 1; + + &.list-container-leave-active { + opacity: 0; + transition: opacity 0.25s ease-out; + } +} diff --git a/src/shared/components/MemberSearch/LoadMoreButton/index.jsx b/src/shared/components/MemberSearch/LoadMoreButton/index.jsx new file mode 100644 index 0000000000..03c54cf1f0 --- /dev/null +++ b/src/shared/components/MemberSearch/LoadMoreButton/index.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './style.scss'; + +const LoadMoreButton = ({ callback, loading }) => ( + +); + + +LoadMoreButton.propTypes = { + callback: PropTypes.func.isRequired, + loading: PropTypes.bool, +}; + +LoadMoreButton.defaultProps = { + loading: false, +}; + +export default LoadMoreButton; diff --git a/src/shared/components/MemberSearch/LoadMoreButton/style.scss b/src/shared/components/MemberSearch/LoadMoreButton/style.scss new file mode 100644 index 0000000000..864d0ec652 --- /dev/null +++ b/src/shared/components/MemberSearch/LoadMoreButton/style.scss @@ -0,0 +1,15 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.load-more { + display: block; + height: 40px; + width: 100%; + max-width: 960px; + margin: 0 auto 20px; + background-color: $tc-gray-40; + border: 1px solid $tc-gray-50; + border-radius: 2px; + font-family: "Roboto", Helvetica, Arial, sans-serif; + font-size: 12px; + color: $tc-gray-neutral-light; +} diff --git a/src/shared/components/MemberSearch/LoadingListItem/index.jsx b/src/shared/components/MemberSearch/LoadingListItem/index.jsx new file mode 100644 index 0000000000..e7ff1066cf --- /dev/null +++ b/src/shared/components/MemberSearch/LoadingListItem/index.jsx @@ -0,0 +1,50 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import './style.scss'; + +const LoadingListItem = ({ type }) => { + switch (type) { + case 'MEMBER': + return ( +
    +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + ); + default: + return null; + } +}; + +LoadingListItem.propTypes = { + type: PropTypes.string.isRequired, +}; + +export default LoadingListItem; diff --git a/src/shared/components/MemberSearch/LoadingListItem/style.scss b/src/shared/components/MemberSearch/LoadingListItem/style.scss new file mode 100644 index 0000000000..15681625dd --- /dev/null +++ b/src/shared/components/MemberSearch/LoadingListItem/style.scss @@ -0,0 +1,133 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.loading-list-item { + display: flex; + max-width: 960px; + border-bottom: 1px solid rgba(163, 163, 174, 0.3); + + @media (max-width: 700px) { + display: block; + } + + &:last-child { + border-bottom: none; + } + + .user-info { + background-color: $tc-white; + flex-basis: 60%; + padding: 20px; + position: relative; + + @media (max-width: 700px) { + flex-basis: 50%; + } + } + + .user-profile { + display: flex; + align-items: center; + + .user-avatar { + width: 60px; + height: 60px; + margin-right: 20px; + position: relative; + border-radius: 50%; + background-color: $tc-gray-10; + + @media (max-width: 700px) { + margin-right: 10px; + } + } + + .username-and-details { + max-width: 225px; + } + + .username { + height: 18px; + width: 126px; + margin-bottom: 10px; + background-color: $tc-gray-10; + + @media (max-width: 700px) { + max-width: 180px; + } + } + + .user-details { + .country-and-wins { + height: 12px; + width: 238px; + background-color: $tc-gray-neutral-light; + margin-bottom: 3px; + } + + .member-since { + height: 12px; + width: 238px; + background-color: $tc-gray-neutral-light; + } + } + } + + .user-stats { + display: flex; + align-items: center; + flex-basis: 50%; + max-width: 440px; + margin-left: auto; + padding: 20px; + background-color: $tc-gray-neutral-light; + border-left: 1px solid $tc-gray-neutral-dark; + + @media (max-width: 700px) { + flex-basis: 100%; + display: block; + width: 100%; + max-width: 1000px; + padding: 15px; + border: none; + border-top: 1px solid $tc-gray-neutral-dark; + overflow-x: auto; + } + } + + .tag-list { + height: 12px; + width: 245px; + background-color: $tc-gray-10; + } + + .track-list { + display: flex; + flex-wrap: wrap; + white-space: nowrap; + + @media (max-width: 700px) { + flex-wrap: nowrap; + display: block; + width: auto; + white-space: nowrap; + } + } + + .track-item { + display: inline-block; + height: 26px; + width: 70px; + margin-top: 10px; + margin-right: 5px; + border-radius: 13px; + background-color: $tc-gray-10; + + &:last-child { + margin-right: 0; + + @media (max-width: 700px) { + margin-right: 15px; + } + } + } +} diff --git a/src/shared/components/MemberSearch/MemberItem/UserInfo/index.jsx b/src/shared/components/MemberSearch/MemberItem/UserInfo/index.jsx new file mode 100644 index 0000000000..3ad3860a42 --- /dev/null +++ b/src/shared/components/MemberSearch/MemberItem/UserInfo/index.jsx @@ -0,0 +1,62 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import UserAvatar from '../../User/UserAvatar'; +import UserBio from '../../User/UserBio'; +import UsernameAndDetails from '../../User/UsernameAndDetails'; + +import './style.scss'; + +const UserInfo = ({ user, userPlace, withBio }) => { + let userBio; + + if (withBio && user.description) { + userBio = ; + } + + let userPlaceNumber = null; + if (_.isFinite(userPlace)) { + userPlaceNumber =
    {userPlace + 1}
    ; + } + + return ( +
    +
    + {userPlaceNumber} + + + + +
    + + {userBio} +
    + ); +}; + +UserInfo.propTypes = { + user: PropTypes.shape({ + maxRating: PropTypes.shape({ + rating: PropTypes.number, + }), + photoURL: PropTypes.string, + handle: PropTypes.string, + competitionCountryCode: PropTypes.string, + wins: PropTypes.oneOf([PropTypes.number, null, undefined]), + createdAt: PropTypes.string, + description: PropTypes.string, + }).isRequired, + userPlace: PropTypes.number.isRequired, + withBio: PropTypes.bool.isRequired, +}; + +export default UserInfo; diff --git a/src/shared/components/MemberSearch/MemberItem/UserInfo/style.scss b/src/shared/components/MemberSearch/MemberItem/UserInfo/style.scss new file mode 100644 index 0000000000..4e654220f2 --- /dev/null +++ b/src/shared/components/MemberSearch/MemberItem/UserInfo/style.scss @@ -0,0 +1,32 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.user-info { + background-color: $tc-white; + flex-basis: 60%; + padding: 20px; + position: relative; + + @media (max-width: 700px) { + flex-basis: 50%; + } +} + +.user-profile { + display: flex; + align-items: center; + + .list-number { + display: inline-block; + width: 40px; + vertical-align: top; + font-family: "Roboto", Helvetica, Arial, sans-serif; + font-size: 22px; + line-height: 28px; + color: $tc-gray-20; + background-color: $tc-white; + + @media (max-width: 700px) { + width: 25px; + } + } +} diff --git a/src/shared/components/MemberSearch/MemberItem/UserStats/index.jsx b/src/shared/components/MemberSearch/MemberItem/UserStats/index.jsx new file mode 100644 index 0000000000..54c5b781f2 --- /dev/null +++ b/src/shared/components/MemberSearch/MemberItem/UserStats/index.jsx @@ -0,0 +1,56 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import { connect } from 'react-redux'; +import TagList from '../../TagList'; +import SubtrackList from '../../SubtrackList'; +import TrackList from '../../TrackList'; +import { getMostRecentSubtracks, sortSkillsByScoreAndTag } from '../../helpers'; + +import './style.scss'; + +const UserStats = ({ member, userPlace, searchTermTag }) => { + let userStatsList; + + const subtracks = getMostRecentSubtracks(member.stats, 5); + + if (subtracks.length) { + userStatsList = ; + } else { + userStatsList = ; + } + + // Highlight the skill that was searched for if the user has it + // but only in the leaderboard, which is indicated by having userPlace + const tag = _.isFinite(userPlace) ? searchTermTag : null; + + const skills = sortSkillsByScoreAndTag(member.skills, tag, 4); + + return ( +
    +
    + + + {userStatsList} +
    +
    + ); +}; + +UserStats.propTypes = { + member: PropTypes.shape({ + skills: PropTypes.arrayOf({}), + tracks: PropTypes.arrayOf(PropTypes.string), + stats: PropTypes.shape({}), + }).isRequired, + userPlace: PropTypes.number.isRequired, + searchTermTag: PropTypes.shape({}), +}; + +UserStats.defaultProps = { + searchTermTag: null, +}; + +const mapStateToProps = ({ memberSearch }) => ({ searchTermTag: memberSearch.searchTermTag }); + +export default connect(mapStateToProps)(UserStats); diff --git a/src/shared/components/MemberSearch/MemberItem/UserStats/style.scss b/src/shared/components/MemberSearch/MemberItem/UserStats/style.scss new file mode 100644 index 0000000000..10a913540f --- /dev/null +++ b/src/shared/components/MemberSearch/MemberItem/UserStats/style.scss @@ -0,0 +1,28 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.user-stats { + display: flex; + align-items: center; + flex-basis: 50%; + max-width: 440px; + margin-left: auto; + padding: 20px; + background-color: $tc-gray-neutral-light; + border-left: 1px solid $tc-gray-neutral-dark; + font-size: 12px; + line-height: 14px; + + @media (max-width: 700px) { + flex-basis: 100%; + display: block; + width: 100%; + max-width: 1000px; + padding: 15px; + border: none; + border-top: 1px solid $tc-gray-neutral-dark; + overflow-x: auto; + -webkit-overflow-scrolling: touch; + font-size: 14px; + line-height: 16px; + } +} diff --git a/src/shared/components/MemberSearch/MemberItem/index.jsx b/src/shared/components/MemberSearch/MemberItem/index.jsx new file mode 100644 index 0000000000..976d617f38 --- /dev/null +++ b/src/shared/components/MemberSearch/MemberItem/index.jsx @@ -0,0 +1,57 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import classNames from 'classnames'; +import { config } from 'topcoder-react-utils'; +import UserInfo from './UserInfo'; +import UserStats from './UserStats'; + +import './style.scss'; + +const MemberItem = ({ + member, + userPlace, + withBio, + shouldAnimate = false, +}) => { + const memberItemStyles = classNames( + 'member-item', + { 'with-bio': withBio }, + ); + + const memberItem = ( + + + + + + ); + + if (shouldAnimate) { + return ( + + {memberItem} + + ); + } + + return memberItem; +}; + +MemberItem.propTypes = { + member: PropTypes.shape({}).isRequired, + userPlace: PropTypes.number, + withBio: PropTypes.bool, + shouldAnimate: PropTypes.bool, +}; + +export default MemberItem; diff --git a/src/shared/components/MemberSearch/MemberItem/style.scss b/src/shared/components/MemberSearch/MemberItem/style.scss new file mode 100644 index 0000000000..d7c563556a --- /dev/null +++ b/src/shared/components/MemberSearch/MemberItem/style.scss @@ -0,0 +1,31 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.member-item { + display: flex; + max-width: 960px; + border-bottom: 1px solid rgba(163, 163, 174, 0.3); + + @media (max-width: 700px) { + display: block; + } + + &:not(.with-bio):last-child { + border-bottom: none; + } +} + +.with-bio { + margin: 20px auto 0; + border: 1px solid rgba(163, 163, 174, 0.3); + box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.13); +} + +// ReactCSSTransitionGroup transitions +.member-item-appear { + opacity: 0.01; + + &.member-item-appear-active { + opacity: 1; + transition: opacity 0.25s ease-in; + } +} diff --git a/src/shared/components/MemberSearch/MemberList/index.jsx b/src/shared/components/MemberSearch/MemberList/index.jsx new file mode 100644 index 0000000000..77c8d0f18c --- /dev/null +++ b/src/shared/components/MemberSearch/MemberList/index.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import MemberItem from '../MemberItem'; + +import './style.scss'; + +const MemberList = (({ members }) => ( +
    + { + members.map(member => ) + } +
    +)); + +MemberList.propTypes = { + members: PropTypes.arrayOf(PropTypes.object).isRequired, +}; + +export default MemberList; diff --git a/src/shared/components/MemberSearch/MemberList/style.scss b/src/shared/components/MemberSearch/MemberList/style.scss new file mode 100644 index 0000000000..6ac3c29cef --- /dev/null +++ b/src/shared/components/MemberSearch/MemberList/style.scss @@ -0,0 +1,10 @@ +.member-list { + .username { + font-size: 16px; + line-height: 19px; + } + + .user-details { + line-height: 14px; + } +} diff --git a/src/shared/components/MemberSearch/MemberSearchView/index.jsx b/src/shared/components/MemberSearch/MemberSearchView/index.jsx new file mode 100644 index 0000000000..4a42cce6e3 --- /dev/null +++ b/src/shared/components/MemberSearch/MemberSearchView/index.jsx @@ -0,0 +1,226 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import ListContainer from '../ListContainer'; +import TopMemberList from '../TopMemberList'; +import MemberList from '../MemberList'; +import MemberItem from '../MemberItem'; +import LoadingListItem from '../LoadingListItem'; +import PageError from '../PageError'; +import NoResults from '../NoResults'; +import LoadMoreButton from '../LoadMoreButton'; +import EndOfResults from '../EndOfResults'; +import { getSearchTagPreposition } from '../helpers'; + +import './style.scss'; + +const MemberSearchView = (props) => { + const { pageLoaded, loadingMore, error } = props; + const { + usernameMatches, + totalCount, + topMembers, + moreMatchesAvailable, + loadMemberSearch, + } = props; + const { previousSearchTerm: searchTerm, searchTermTag: tag } = props; + + function renderPageState() { + let result = null; + if (error) { + result = ( + + + + ); + } else if (searchTerm && pageLoaded && !usernameMatches.length && !topMembers.length) { + result = ( + + + + ); + } else if (!pageLoaded && !usernameMatches.length && !topMembers.length) { + const loadingListItems = []; + + for (let i = 0; i < 10; i += 1) { + loadingListItems.push(); + } + + result = ( + + +
      + {loadingListItems} +
    +
    +
    + ); + } + + return result; + } + + function renderTopMembers() { + if (pageLoaded && tag && topMembers.length) { + const preposition = getSearchTagPreposition(tag.domain); + + return ( + + + + ); + } + + return null; + } + + function renderUsernameMatches() { + let memberMatches; + let exactMemberMatch; + let restOfUsernameMatches; + + if (pageLoaded && usernameMatches.length) { + // Check if the first member in the array matches the search term + const isSearchTerm = _.isString(searchTerm); + const isExactMatch = isSearchTerm + && usernameMatches[0].handle.toLowerCase() === searchTerm.toLowerCase(); + + // If it's an exact match, and there is no leaderboard, + // show the exact match separately + if (isExactMatch && !tag) { + exactMemberMatch = ; + + restOfUsernameMatches = usernameMatches.slice(1); + } + + // If there is an exact match and no other matching usernames + if (restOfUsernameMatches && restOfUsernameMatches.length === 0) { + memberMatches = null; + } else { + memberMatches = ( + + + + + + ); + } + } + + return { + exactMemberMatch, + memberMatches, + }; + } + + function renderLoadMoreButton() { + const loadMoreMembers = () => { + loadMemberSearch(searchTerm); + }; + + if (moreMatchesAvailable && pageLoaded && !loadingMore + && !error && usernameMatches.length === 10) { + return ; + } + + if (moreMatchesAvailable && loadingMore && !error && usernameMatches.length === 10) { + return ; + } + + return null; + } + + function renderEndOfResults() { + const numResults = usernameMatches.length; + + // Don't show 'End of results' if the page is loading + let result; + if (!pageLoaded) { + result = null; + } else if (numResults !== totalCount) { // Or if there are more members to load + result = null; + } else if (numResults === 0 && topMembers.length === 0) { // Or if there are no results at all + result = null; + } else { + result = ; + } + + return result; + } + + const { + exactMemberMatch: exactMemberMatchItem, memberMatches: memberMatchItems, + } = renderUsernameMatches(); + const topMemberLeaderboard = renderTopMembers(); + const pageStatus = renderPageState(); + const loadMoreButton = renderLoadMoreButton(); + const endOfResults = renderEndOfResults(); + + return ( +
    + {pageStatus} + + {exactMemberMatchItem} + + {topMemberLeaderboard} + + {memberMatchItems} + + {loadMoreButton} + + {endOfResults} +
    + ); +}; + +MemberSearchView.propTypes = { + pageLoaded: PropTypes.bool.isRequired, + loadingMore: PropTypes.bool.isRequired, + error: PropTypes.bool.isRequired, + + usernameMatches: PropTypes.arrayOf({}).isRequired, + moreMatchesAvailable: PropTypes.bool.isRequired, + totalCount: PropTypes.number.isRequired, + topMembers: PropTypes.shape([]).isRequired, + + previousSearchTerm: PropTypes.string.isRequired, + searchTermTag: PropTypes.shape({}).isRequired, + + loadMemberSearch: PropTypes.func.isRequired, +}; + +export default MemberSearchView; diff --git a/src/shared/components/MemberSearch/MemberSearchView/style.scss b/src/shared/components/MemberSearch/MemberSearchView/style.scss new file mode 100644 index 0000000000..aac41eea98 --- /dev/null +++ b/src/shared/components/MemberSearch/MemberSearchView/style.scss @@ -0,0 +1,10 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.member-search-view { + margin: 0 32px; + background-color: $tc-gray-neutral-dark; + + @media (max-width: 700px) { + margin: 0 10px; + } +} diff --git a/src/shared/components/MemberSearch/NoResults/index.jsx b/src/shared/components/MemberSearch/NoResults/index.jsx new file mode 100644 index 0000000000..cdb6586b69 --- /dev/null +++ b/src/shared/components/MemberSearch/NoResults/index.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import RobotIcon from '../icons/RobotIcon'; + +import './style.scss'; + +const NoResults = ({ entry }) => ( +
    +

    Sorry, no results found for {entry}

    + + +
    +); + + +NoResults.propTypes = { + entry: PropTypes.string.isRequired, +}; + +export default NoResults; diff --git a/src/shared/components/MemberSearch/NoResults/style.scss b/src/shared/components/MemberSearch/NoResults/style.scss new file mode 100644 index 0000000000..f983b9048e --- /dev/null +++ b/src/shared/components/MemberSearch/NoResults/style.scss @@ -0,0 +1,32 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.no-results { + max-width: 960px; + margin: 70px auto 20px; + text-align: center; + color: $tc-gray-80; + + p { + margin-bottom: 50px; + + @include tc-heading-large; + + font-family: "Roboto", Helvetica, Arial, sans-serif; + font-weight: 300; + } + + span:last-child { + font-weight: 500; + word-break: break-word; + } +} + +// ReactCSSTransitionGroup transitions +.no-results-appear { + opacity: 0.01; + + &.no-results-appear-active { + opacity: 1; + transition: opacity 0.25s ease-in; + } +} diff --git a/src/shared/components/MemberSearch/PageError/index.jsx b/src/shared/components/MemberSearch/PageError/index.jsx new file mode 100644 index 0000000000..00c5207b83 --- /dev/null +++ b/src/shared/components/MemberSearch/PageError/index.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import RobotIcon from '../icons/RobotIcon'; + +import './style.scss'; + +const PageError = () => ( +
    +

    Oops! There was an error.

    + + +
    +); + + +export default PageError; diff --git a/src/shared/components/MemberSearch/PageError/style.scss b/src/shared/components/MemberSearch/PageError/style.scss new file mode 100644 index 0000000000..c447f143ca --- /dev/null +++ b/src/shared/components/MemberSearch/PageError/style.scss @@ -0,0 +1,27 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.page-error { + max-width: 960px; + margin: 70px auto 20px; + text-align: center; + color: $tc-gray-80; + + p { + margin-bottom: 50px; + + @include tc-heading-large; + + font-family: "Roboto", Helvetica, Arial, sans-serif; + font-weight: 300; + } +} + +// ReactCSSTransitionGroup transitions +.page-error-appear { + opacity: 0.01; + + &.page-error-appear-active { + opacity: 1; + transition: opacity 0.25s ease-in; + } +} diff --git a/src/shared/components/MemberSearch/SubtrackList/SubtrackItem/index.jsx b/src/shared/components/MemberSearch/SubtrackList/SubtrackItem/index.jsx new file mode 100644 index 0000000000..71ad51c89c --- /dev/null +++ b/src/shared/components/MemberSearch/SubtrackList/SubtrackItem/index.jsx @@ -0,0 +1,53 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { + getSubtrackAbbreviation, + getRoundedPercentage, + numberWithCommas, +} from '../../helpers'; +import TrophyIcon from '../../icons/TrophyIcon'; + + +import './style.scss'; + +const SubtrackItem = ({ subtrack }) => { + const subtrackStyles = classNames( + 'subtrack-item', + `track-${subtrack.track}`, + ); + + const statType = subtrack.stat.type; + let statValue = subtrack.stat.value; + + statValue = statType === 'fulfillment' + ? getRoundedPercentage(statValue) + : numberWithCommas(statValue); + + const trophyIcon = statType === 'wins' ? : null; + + return ( + + + {trophyIcon} + + {statValue} + + + {getSubtrackAbbreviation(subtrack.name)} + + ); +}; + +SubtrackItem.propTypes = { + subtrack: PropTypes.shape({ + track: PropTypes.string, + stat: PropTypes.shape({ + type: PropTypes.string, + value: PropTypes.number, + }), + name: PropTypes.string, + }).isRequired, +}; + +export default SubtrackItem; diff --git a/src/shared/components/MemberSearch/SubtrackList/SubtrackItem/style.scss b/src/shared/components/MemberSearch/SubtrackList/SubtrackItem/style.scss new file mode 100644 index 0000000000..390ffd0e10 --- /dev/null +++ b/src/shared/components/MemberSearch/SubtrackList/SubtrackItem/style.scss @@ -0,0 +1,52 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.subtrack-item { + display: inline-block; + padding: 3px 10px; + margin-top: 5px; + margin-right: 5px; + border-radius: 13px; + + @include tc-label-small; + + font-weight: 500; + + &:last-child { + margin-right: 0; + + @media (max-width: 700px) { + margin-right: 15px; + } + } + + &.track-DESIGN { + background-color: $tc-light_blue; + } + + &.track-DEVELOP { + background-color: $tc-green; + } + + &.track-DATA_SCIENCE { + background-color: $tc-orange; + } + + &.track-COPILOT { + background-color: $tc-gray-70; + } + + .subtrack-wins { + @include tc-label; + + color: $tc-white; + + svg { + margin-right: 2px; + } + } + + .track-code { + color: rgba($tc-white, 0.7); + margin-left: 3px; + } +} diff --git a/src/shared/components/MemberSearch/SubtrackList/index.jsx b/src/shared/components/MemberSearch/SubtrackList/index.jsx new file mode 100644 index 0000000000..e3f994134d --- /dev/null +++ b/src/shared/components/MemberSearch/SubtrackList/index.jsx @@ -0,0 +1,17 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import SubtrackItem from './SubtrackItem'; + +import './style.scss'; + +const SubtrackList = ({ subtracks }) => ( +
    + {subtracks.map(s => )} +
    +); + +SubtrackList.propTypes = { + subtracks: PropTypes.shape([]).isRequired, +}; + +export default SubtrackList; diff --git a/src/shared/components/MemberSearch/SubtrackList/style.scss b/src/shared/components/MemberSearch/SubtrackList/style.scss new file mode 100644 index 0000000000..8462715e27 --- /dev/null +++ b/src/shared/components/MemberSearch/SubtrackList/style.scss @@ -0,0 +1,12 @@ +.subtracks-list { + display: flex; + flex-wrap: wrap; + width: 100%; + + @media (max-width: 700px) { + flex-wrap: nowrap; + display: block; + width: auto; + white-space: nowrap; + } +} diff --git a/src/shared/components/MemberSearch/TagList/TagItem/index.jsx b/src/shared/components/MemberSearch/TagList/TagItem/index.jsx new file mode 100644 index 0000000000..ae8c8663a2 --- /dev/null +++ b/src/shared/components/MemberSearch/TagList/TagItem/index.jsx @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import './style.scss'; + +const TagItem = ({ tag }) => { + const tagItemStyles = classNames( + 'tag-text', + { 'searched-tag': tag.searchedTag }, + { 'special-tag': tag.specialTag }, + ); + + return ( + # + {tag.name} + + ); +}; + +TagItem.propTypes = { + tag: PropTypes.shape({ + searchedTag: PropTypes.string, + specialTag: PropTypes.string, + name: PropTypes.string, + }).isRequired, +}; + +export default TagItem; diff --git a/src/shared/components/MemberSearch/TagList/TagItem/style.scss b/src/shared/components/MemberSearch/TagList/TagItem/style.scss new file mode 100644 index 0000000000..9e05094694 --- /dev/null +++ b/src/shared/components/MemberSearch/TagList/TagItem/style.scss @@ -0,0 +1,37 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.tag-item { + margin-bottom: 5px; + margin-right: 5px; + + @include tc-label; + + font-weight: 400; + color: $tc-gray-30; + + &:last-child { + margin-right: 0; + + @media (max-width: 700px) { + margin-right: 15px; + } + } + + .tag-text { + color: $tc-gray-50; + } + + .searched-tag { + color: $tc-black; + } + + .special-tag { + color: $tc-purple-70; + } + + // Commented out since skills are not yet clickable + // &:hover, + // &:hover .tag-text { + // color: $primary; + // } +} diff --git a/src/shared/components/MemberSearch/TagList/index.jsx b/src/shared/components/MemberSearch/TagList/index.jsx new file mode 100644 index 0000000000..426a175ab1 --- /dev/null +++ b/src/shared/components/MemberSearch/TagList/index.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import TagItem from './TagItem'; + +import './style.scss'; + +const TagList = ({ tags, label, emptyMessage = '' }) => { + const tagListStyles = classNames( + 'tag-list', + { 'no-tags': !tags.length }, + ); + + const tagLabelStyles = classNames({ 'tag-list-label': tags.length && label }); + + const tagLabel = tags.length && label ? label : null; + + const noTagsMessage = !tags.length && emptyMessage ? emptyMessage : null; + + const tagItems = tags.map(t => ); + + return ( +
    + {tagLabel} + + {noTagsMessage} + + {tagItems} +
    + ); +}; + +TagList.propTypes = { + tags: PropTypes.shape([]).isRequired, + label: PropTypes.string, + emptyMessage: PropTypes.string.isRequired, +}; + +TagList.defaultProps = { + label: '', +}; + +export default TagList; diff --git a/src/shared/components/MemberSearch/TagList/style.scss b/src/shared/components/MemberSearch/TagList/style.scss new file mode 100644 index 0000000000..5766cbe820 --- /dev/null +++ b/src/shared/components/MemberSearch/TagList/style.scss @@ -0,0 +1,29 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.tag-list { + display: flex; + flex-wrap: wrap; + white-space: nowrap; + + @include tc-label; + + font-weight: 400; + + @media (max-width: 700px) { + flex-wrap: nowrap; + display: block; + width: auto; + white-space: nowrap; + } + + &.no-tags { + margin-bottom: 5px; + font-style: italic; + color: $tc-gray-20; + } +} + +.tag-list-label { + display: inline-block; + margin-right: 5px; +} diff --git a/src/shared/components/MemberSearch/TopMemberList/index.jsx b/src/shared/components/MemberSearch/TopMemberList/index.jsx new file mode 100644 index 0000000000..89039560f5 --- /dev/null +++ b/src/shared/components/MemberSearch/TopMemberList/index.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import MemberItem from '../MemberItem'; + +const TopMemberList = ({ topMembers }) => { + const sortedTopMembers = _.orderBy(topMembers, 'wins', 'desc'); + + const topMemberItems = sortedTopMembers.map((member, i) => ( + + )); + + return ( +
    + {topMemberItems} +
    + ); +}; + +TopMemberList.propTypes = { + topMembers: PropTypes.arrayOf(PropTypes.object).isRequired, +}; + +export default TopMemberList; diff --git a/src/shared/components/MemberSearch/TrackList/TrackItem/index.jsx b/src/shared/components/MemberSearch/TrackList/TrackItem/index.jsx new file mode 100644 index 0000000000..a4797c650a --- /dev/null +++ b/src/shared/components/MemberSearch/TrackList/TrackItem/index.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +import './style.scss'; + +const TrackItem = ({ track }) => { + const trackStyles = classNames( + 'track-item', + { [`track-${track.toLowerCase()}`]: track.length }, + { 'no-track': !track.length }, + ); + + const trackMap = { + DEVELOP: 'Developer', + DESIGN: 'Designer', + DATA_SCIENCE: 'Data Scientist', + }; + + const trackName = trackMap[track]; + + return ( + + {trackName || 'No track selected'} + + ); +}; + +TrackItem.propTypes = { + track: PropTypes.string.isRequired, +}; + +export default TrackItem; diff --git a/src/shared/components/MemberSearch/TrackList/TrackItem/style.scss b/src/shared/components/MemberSearch/TrackList/TrackItem/style.scss new file mode 100644 index 0000000000..413cc5f8b8 --- /dev/null +++ b/src/shared/components/MemberSearch/TrackList/TrackItem/style.scss @@ -0,0 +1,46 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.track-item { + display: inline-block; + padding: 5px 10px; + margin-top: 5px; + margin-right: 5px; + border-radius: 13px; + font-weight: 600; + color: $tc-white; + + &:last-child { + margin-right: 0; + + @media (max-width: 700px) { + margin-right: 15px; + } + } + + &.track-design { + background-color: $tc-light_blue; + } + + &.track-develop { + background-color: $tc-green; + } + + &.track-data_science { + background-color: $tc-orange; + } + + &.track-copilot { + background-color: $tc-gray-70; + font-size: 14px; + } + + &.no-track { + font-weight: 500; + background-color: $tc-gray-20; + } + + .track-name { + font-size: 14px; + line-height: 16px; + } +} diff --git a/src/shared/components/MemberSearch/TrackList/index.jsx b/src/shared/components/MemberSearch/TrackList/index.jsx new file mode 100644 index 0000000000..0dd4ce1b47 --- /dev/null +++ b/src/shared/components/MemberSearch/TrackList/index.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import TrackItem from './TrackItem'; + +import './style.scss'; + +const TrackList = ({ tracks }) => { + let trackItems; + if (tracks.length) { + trackItems = tracks.map(t => ); + } else { + trackItems = ; + } + + return ( +
    + {trackItems} +
    + ); +}; + +TrackList.propTypes = { + tracks: PropTypes.shape([]).isRequired, +}; + +export default TrackList; diff --git a/src/shared/components/MemberSearch/TrackList/style.scss b/src/shared/components/MemberSearch/TrackList/style.scss new file mode 100644 index 0000000000..6636408392 --- /dev/null +++ b/src/shared/components/MemberSearch/TrackList/style.scss @@ -0,0 +1,12 @@ +.track-list { + display: flex; + flex-wrap: wrap; + white-space: nowrap; + + @media (max-width: 700px) { + flex-wrap: nowrap; + display: block; + width: auto; + white-space: nowrap; + } +} diff --git a/src/shared/components/MemberSearch/User/UserAvatar/default-avatar.svg b/src/shared/components/MemberSearch/User/UserAvatar/default-avatar.svg new file mode 100644 index 0000000000..9a5883f8e0 --- /dev/null +++ b/src/shared/components/MemberSearch/User/UserAvatar/default-avatar.svg @@ -0,0 +1,20 @@ + + + + ico-user-default + Created with Sketch. + + + + + + + + + + + + + + + diff --git a/src/shared/components/MemberSearch/User/UserAvatar/index.jsx b/src/shared/components/MemberSearch/User/UserAvatar/index.jsx new file mode 100644 index 0000000000..2e1bf7601a --- /dev/null +++ b/src/shared/components/MemberSearch/User/UserAvatar/index.jsx @@ -0,0 +1,43 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import LevelDesignatorIcon from '../../icons/LevelDesignatorIcon'; +import { memberLevelByRating } from '../../helpers'; + +import './style.scss'; + +const UserAvatar = ({ showLevel, rating, photoURL }) => { + let levelIcon; + + if (showLevel) { + levelIcon = ; + } + + /* eslint-disable global-require */ + let backgroundImageUrl = `url(${require('./default-avatar.svg')})`; + + if (photoURL) { + backgroundImageUrl = `url(${photoURL}), ${backgroundImageUrl}`; + } + + // Delete -r when taking member search back out of the angular app + // Renamed to -r to avoid naming collisions + return ( +
    + {levelIcon} +
    + ); +}; + + +UserAvatar.propTypes = { + showLevel: PropTypes.bool, + rating: PropTypes.number.isRequired, + photoURL: PropTypes.string, +}; + +UserAvatar.defaultProps = { + showLevel: '', + photoURL: '', +}; + +export default UserAvatar; diff --git a/src/shared/components/MemberSearch/User/UserAvatar/style.scss b/src/shared/components/MemberSearch/User/UserAvatar/style.scss new file mode 100644 index 0000000000..1556476641 --- /dev/null +++ b/src/shared/components/MemberSearch/User/UserAvatar/style.scss @@ -0,0 +1,23 @@ +@import '~tc-ui/src/styles/tc-includes'; + +// Delete -r when taking member search back out of the angular app +// Renamed to -r to avoid naming collisions +.user-avatar-r { + width: 60px; + height: 60px; + margin-right: 20px; + position: relative; + border-radius: 50%; + background-size: cover; + background-position: center; + + @media (max-width: 700px) { + margin-right: 10px; + } + + .user-rank { + position: absolute; + top: 0; + right: 0; + } +} diff --git a/src/shared/components/MemberSearch/User/UserBio/index.jsx b/src/shared/components/MemberSearch/User/UserBio/index.jsx new file mode 100644 index 0000000000..3b3b159cd2 --- /dev/null +++ b/src/shared/components/MemberSearch/User/UserBio/index.jsx @@ -0,0 +1,20 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Dotdotdot from 'react-dotdotdot'; + +import './style.scss'; + +const UserBio = ({ bio }) => ( +
    + + {bio} + +
    +); + + +UserBio.propTypes = { + bio: PropTypes.string.isRequired, +}; + +export default UserBio; diff --git a/src/shared/components/MemberSearch/User/UserBio/style.scss b/src/shared/components/MemberSearch/User/UserBio/style.scss new file mode 100644 index 0000000000..4561e9fc91 --- /dev/null +++ b/src/shared/components/MemberSearch/User/UserBio/style.scss @@ -0,0 +1,9 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.user-bio { + @include tc-body-small; + + font-family: "Roboto", Helvetica, Arial, sans-serif; + color: $tc-gray-80; + margin-top: 20px; +} diff --git a/src/shared/components/MemberSearch/User/UsernameAndDetails/index.jsx b/src/shared/components/MemberSearch/User/UsernameAndDetails/index.jsx new file mode 100644 index 0000000000..1f60c712dd --- /dev/null +++ b/src/shared/components/MemberSearch/User/UsernameAndDetails/index.jsx @@ -0,0 +1,51 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import _ from 'lodash'; +import moment from 'moment'; +import { singlePluralFormatter } from '../../helpers'; +import ISOCountries from '../../helpers/ISOCountries'; + +import './style.scss'; + +const UsernameAndDetails = ({ + username, + country, + numWins, + memberSince, +}) => { + const countryObject = _.find(ISOCountries, { alpha3: country }); + const userCountry = countryObject ? countryObject.name : ''; + + const numberWins = singlePluralFormatter(numWins, 'win'); + + const memberSinceMMMYYYY = moment(memberSince).format('MMM YYYY'); + + return ( +
    +

    + {username} +

    + +
    +
    + {userCountry} + + {numberWins && ` / ${numberWins}`} +
    + +
    + Member since {memberSinceMMMYYYY} +
    +
    +
    + ); +}; + +UsernameAndDetails.propTypes = { + username: PropTypes.string.isRequired, + country: PropTypes.string.isRequired, + numWins: PropTypes.number.isRequired, + memberSince: PropTypes.number.isRequired, +}; + +export default UsernameAndDetails; diff --git a/src/shared/components/MemberSearch/User/UsernameAndDetails/style.scss b/src/shared/components/MemberSearch/User/UsernameAndDetails/style.scss new file mode 100644 index 0000000000..535de5e8bf --- /dev/null +++ b/src/shared/components/MemberSearch/User/UsernameAndDetails/style.scss @@ -0,0 +1,36 @@ +@import '~tc-ui/src/styles/tc-includes'; + +.username-and-details { + max-width: 225px; + + .username { + @include tc-heading; + + font-family: "Roboto", Helvetica, Arial, sans-serif; + color: $tc-gray-80; + text-overflow: ellipsis; + overflow: hidden; + + @media (max-width: 700px) { + max-width: 180px; + } + } + + .user-details { + .country-and-wins { + @include tc-label-small; + + color: $tc-gray-60; + + .user-country { + text-transform: uppercase; + } + } + + .member-since { + @include tc-label-small; + + color: $tc-gray-30; + } + } +} diff --git a/src/shared/components/MemberSearch/helpers/ISOCountries.js b/src/shared/components/MemberSearch/helpers/ISOCountries.js new file mode 100644 index 0000000000..93bd801c9d --- /dev/null +++ b/src/shared/components/MemberSearch/helpers/ISOCountries.js @@ -0,0 +1,751 @@ +const ISOCountries = [ + { + alpha2: 'AF', alpha3: 'AFG', code: 4, numericString: '004', name: 'Afghanistan', + }, + { + alpha2: 'AX', alpha3: 'ALA', code: 248, numericString: '248', name: 'Åland Islands', + }, + { + alpha2: 'AL', alpha3: 'ALB', code: 8, numericString: '008', name: 'Albania', + }, + { + alpha2: 'DZ', alpha3: 'DZA', code: 12, numericString: '012', name: 'Algeria', + }, + { + alpha2: 'AS', alpha3: 'ASM', code: 16, numericString: '016', name: 'American Samoa', + }, + { + alpha2: 'AD', alpha3: 'AND', code: 20, numericString: '020', name: 'Andorra', + }, + { + alpha2: 'AO', alpha3: 'AGO', code: 24, numericString: '024', name: 'Angola', + }, + { + alpha2: 'AI', alpha3: 'AIA', code: 660, numericString: '660', name: 'Anguilla', + }, + { + alpha2: 'AQ', alpha3: 'ATA', code: 10, numericString: '010', name: 'Antarctica', + }, + { + alpha2: 'AG', alpha3: 'ATG', code: 28, numericString: '028', name: 'Antigua and Barbuda', + }, + { + alpha2: 'AR', alpha3: 'ARG', code: 32, numericString: '032', name: 'Argentina', + }, + { + alpha2: 'AM', alpha3: 'ARM', code: 51, numericString: '051', name: 'Armenia', + }, + { + alpha2: 'AW', alpha3: 'ABW', code: 533, numericString: '533', name: 'Aruba', + }, + { + alpha2: 'AU', alpha3: 'AUS', code: 36, numericString: '036', name: 'Australia', + }, + { + alpha2: 'AT', alpha3: 'AUT', code: 40, numericString: '040', name: 'Austria', + }, + { + alpha2: 'AZ', alpha3: 'AZE', code: 31, numericString: '031', name: 'Azerbaijan', + }, + { + alpha2: 'BS', alpha3: 'BHS', code: 44, numericString: '044', name: 'Bahamas', + }, + { + alpha2: 'BH', alpha3: 'BHR', code: 48, numericString: '048', name: 'Bahrain', + }, + { + alpha2: 'BD', alpha3: 'BGD', code: 50, numericString: '050', name: 'Bangladesh', + }, + { + alpha2: 'BB', alpha3: 'BRB', code: 52, numericString: '052', name: 'Barbados', + }, + { + alpha2: 'BY', alpha3: 'BLR', code: 112, numericString: '112', name: 'Belarus', + }, + { + alpha2: 'BE', alpha3: 'BEL', code: 56, numericString: '056', name: 'Belgium', + }, + { + alpha2: 'BZ', alpha3: 'BLZ', code: 84, numericString: '084', name: 'Belize', + }, + { + alpha2: 'BJ', alpha3: 'BEN', code: 204, numericString: '204', name: 'Benin', + }, + { + alpha2: 'BM', alpha3: 'BMU', code: 60, numericString: '060', name: 'Bermuda', + }, + { + alpha2: 'BT', alpha3: 'BTN', code: 64, numericString: '064', name: 'Bhutan', + }, + { + alpha2: 'BO', alpha3: 'BOL', code: 68, numericString: '068', name: 'Bolivia, Plurinational State of', + }, + { + alpha2: 'BQ', alpha3: 'BES', code: 535, numericString: '535', name: 'Bonaire, Sint Eustatius and Saba', + }, + { + alpha2: 'BA', alpha3: 'BIH', code: 70, numericString: '070', name: 'Bosnia and Herzegovina', + }, + { + alpha2: 'BW', alpha3: 'BWA', code: 72, numericString: '072', name: 'Botswana', + }, + { + alpha2: 'BV', alpha3: 'BVT', code: 74, numericString: '074', name: 'Bouvet Island', + }, + { + alpha2: 'BR', alpha3: 'BRA', code: 76, numericString: '076', name: 'Brazil', + }, + { + alpha2: 'IO', alpha3: 'IOT', code: 86, numericString: '086', name: 'British Indian Ocean Territory', + }, + { + alpha2: 'BN', alpha3: 'BRN', code: 96, numericString: '096', name: 'Brunei Darussalam', + }, + { + alpha2: 'BG', alpha3: 'BGR', code: 100, numericString: '100', name: 'Bulgaria', + }, + { + alpha2: 'BF', alpha3: 'BFA', code: 854, numericString: '854', name: 'Burkina Faso', + }, + { + alpha2: 'BI', alpha3: 'BDI', code: 108, numericString: '108', name: 'Burundi', + }, + { + alpha2: 'KH', alpha3: 'CPV', code: 132, numericString: '132', name: 'Cabo Verde', + }, + { + alpha2: 'CM', alpha3: 'KHM', code: 116, numericString: '116', name: 'Cambodia', + }, + { + alpha2: 'CA', alpha3: 'CMR', code: 120, numericString: '120', name: 'Cameroon', + }, + { + alpha2: 'CV', alpha3: 'CAN', code: 124, numericString: '124', name: 'Canada', + }, + { + alpha2: 'KY', alpha3: 'CYM', code: 136, numericString: '136', name: 'Cayman Islands', + }, + { + alpha2: 'CF', alpha3: 'CAF', code: 140, numericString: '140', name: 'Central African Republic', + }, + { + alpha2: 'TD', alpha3: 'TCD', code: 148, numericString: '148', name: 'Chad', + }, + { + alpha2: 'CL', alpha3: 'CHL', code: 152, numericString: '152', name: 'Chile', + }, + { + alpha2: 'CN', alpha3: 'CHN', code: 156, numericString: '156', name: 'China', + }, + { + alpha2: 'CX', alpha3: 'CXR', code: 162, numericString: '162', name: 'Christmas Island', + }, + { + alpha2: 'CC', alpha3: 'CCK', code: 166, numericString: '166', name: 'Cocos (Keeling) Islands', + }, + { + alpha2: 'CO', alpha3: 'COL', code: 170, numericString: '170', name: 'Colombia', + }, + { + alpha2: 'KM', alpha3: 'COM', code: 174, numericString: '174', name: 'Comoros', + }, + { + alpha2: 'CG', alpha3: 'COG', code: 178, numericString: '178', name: 'Congo', + }, + { + alpha2: 'CD', alpha3: 'COD', code: 180, numericString: '180', name: 'Congo, the Democratic Republic of the', + }, + { + alpha2: 'CK', alpha3: 'COK', code: 184, numericString: '184', name: 'Cook Islands', + }, + { + alpha2: 'CR', alpha3: 'CRI', code: 188, numericString: '188', name: 'Costa Rica', + }, + { + alpha2: 'CI', alpha3: 'CIV', code: 384, numericString: '384', name: 'Côte d\'Ivoire', + }, + { + alpha2: 'HR', alpha3: 'HRV', code: 191, numericString: '191', name: 'Croatia', + }, + { + alpha2: 'CU', alpha3: 'CUB', code: 192, numericString: '192', name: 'Cuba', + }, + { + alpha2: 'CW', alpha3: 'CUW', code: 531, numericString: '531', name: 'Curaçao', + }, + { + alpha2: 'CY', alpha3: 'CYP', code: 196, numericString: '196', name: 'Cyprus', + }, + { + alpha2: 'CZ', alpha3: 'CZE', code: 203, numericString: '203', name: 'Czech Republic', + }, + { + alpha2: 'DK', alpha3: 'DNK', code: 208, numericString: '208', name: 'Denmark', + }, + { + alpha2: 'DJ', alpha3: 'DJI', code: 262, numericString: '262', name: 'Djibouti', + }, + { + alpha2: 'DM', alpha3: 'DMA', code: 212, numericString: '212', name: 'Dominica', + }, + { + alpha2: 'DO', alpha3: 'DOM', code: 214, numericString: '214', name: 'Dominican Republic', + }, + { + alpha2: 'EC', alpha3: 'ECU', code: 218, numericString: '218', name: 'Ecuador', + }, + { + alpha2: 'EG', alpha3: 'EGY', code: 818, numericString: '818', name: 'Egypt', + }, + { + alpha2: 'SV', alpha3: 'SLV', code: 222, numericString: '222', name: 'El Salvador', + }, + { + alpha2: 'GQ', alpha3: 'GNQ', code: 226, numericString: '226', name: 'Equatorial Guinea', + }, + { + alpha2: 'ER', alpha3: 'ERI', code: 232, numericString: '232', name: 'Eritrea', + }, + { + alpha2: 'EE', alpha3: 'EST', code: 233, numericString: '233', name: 'Estonia', + }, + { + alpha2: 'ET', alpha3: 'ETH', code: 231, numericString: '231', name: 'Ethiopia', + }, + { + alpha2: 'FK', alpha3: 'FLK', code: 238, numericString: '238', name: 'Falkland Islands (Malvinas)', + }, + { + alpha2: 'FO', alpha3: 'FRO', code: 234, numericString: '234', name: 'Faroe Islands', + }, + { + alpha2: 'FJ', alpha3: 'FJI', code: 242, numericString: '242', name: 'Fiji', + }, + { + alpha2: 'FI', alpha3: 'FIN', code: 246, numericString: '246', name: 'Finland', + }, + { + alpha2: 'FR', alpha3: 'FRA', code: 250, numericString: '250', name: 'France', + }, + { + alpha2: 'GF', alpha3: 'GUF', code: 254, numericString: '254', name: 'French Guiana', + }, + { + alpha2: 'PF', alpha3: 'PYF', code: 258, numericString: '258', name: 'French Polynesia', + }, + { + alpha2: 'TF', alpha3: 'ATF', code: 260, numericString: '260', name: 'French Southern Territories', + }, + { + alpha2: 'GA', alpha3: 'GAB', code: 266, numericString: '266', name: 'Gabon', + }, + { + alpha2: 'GM', alpha3: 'GMB', code: 270, numericString: '270', name: 'Gambia', + }, + { + alpha2: 'GE', alpha3: 'GEO', code: 268, numericString: '268', name: 'Georgia', + }, + { + alpha2: 'DE', alpha3: 'DEU', code: 276, numericString: '276', name: 'Germany', + }, + { + alpha2: 'GH', alpha3: 'GHA', code: 288, numericString: '288', name: 'Ghana', + }, + { + alpha2: 'GI', alpha3: 'GIB', code: 292, numericString: '292', name: 'Gibraltar', + }, + { + alpha2: 'GR', alpha3: 'GRC', code: 300, numericString: '300', name: 'Greece', + }, + { + alpha2: 'GL', alpha3: 'GRL', code: 304, numericString: '304', name: 'Greenland', + }, + { + alpha2: 'GD', alpha3: 'GRD', code: 308, numericString: '308', name: 'Grenada', + }, + { + alpha2: 'GP', alpha3: 'GLP', code: 312, numericString: '312', name: 'Guadeloupe', + }, + { + alpha2: 'GU', alpha3: 'GUM', code: 316, numericString: '316', name: 'Guam', + }, + { + alpha2: 'GT', alpha3: 'GTM', code: 320, numericString: '320', name: 'Guatemala', + }, + { + alpha2: 'GG', alpha3: 'GGY', code: 831, numericString: '831', name: 'Guernsey', + }, + { + alpha2: 'GN', alpha3: 'GIN', code: 324, numericString: '324', name: 'Guinea', + }, + { + alpha2: 'GW', alpha3: 'GNB', code: 624, numericString: '624', name: 'Guinea-Bissau', + }, + { + alpha2: 'GY', alpha3: 'GUY', code: 328, numericString: '328', name: 'Guyana', + }, + { + alpha2: 'HT', alpha3: 'GTI', code: 332, numericString: '332', name: 'Haiti', + }, + { + alpha2: 'HM', alpha3: 'GMD', code: 334, numericString: '334', name: 'Heard Island and McDonald Islands', + }, + { + alpha2: 'VA', alpha3: 'VAT', code: 336, numericString: '336', name: 'Holy See (Vatican City State)', + }, + { + alpha2: 'HN', alpha3: 'HND', code: 340, numericString: '340', name: 'Honduras', + }, + { + alpha2: 'HK', alpha3: 'HKG', code: 344, numericString: '344', name: 'Hong Kong', + }, + { + alpha2: 'HU', alpha3: 'HUN', code: 348, numericString: '348', name: 'Hungary', + }, + { + alpha2: 'IS', alpha3: 'ISL', code: 352, numericString: '352', name: 'Iceland', + }, + { + alpha2: 'IN', alpha3: 'IND', code: 356, numericString: '356', name: 'India', + }, + { + alpha2: 'ID', alpha3: 'IDN', code: 360, numericString: '360', name: 'Indonesia', + }, + { + alpha2: 'IR', alpha3: 'IRN', code: 364, numericString: '364', name: 'Iran, Islamic Republic of', + }, + { + alpha2: 'IQ', alpha3: 'IRQ', code: 368, numericString: '368', name: 'Iraq', + }, + { + alpha2: 'IE', alpha3: 'IRL', code: 372, numericString: '372', name: 'Ireland', + }, + { + alpha2: 'IM', alpha3: 'IMN', code: 833, numericString: '833', name: 'Isle of Man', + }, + { + alpha2: 'IL', alpha3: 'ISR', code: 376, numericString: '376', name: 'Israel', + }, + { + alpha2: 'IT', alpha3: 'ITA', code: 380, numericString: '380', name: 'Italy', + }, + { + alpha2: 'JM', alpha3: 'JAM', code: 388, numericString: '388', name: 'Jamaica', + }, + { + alpha2: 'JP', alpha3: 'JPN', code: 392, numericString: '392', name: 'Japan', + }, + { + alpha2: 'JE', alpha3: 'JEY', code: 832, numericString: '832', name: 'Jersey', + }, + { + alpha2: 'JO', alpha3: 'JOR', code: 400, numericString: '400', name: 'Jordan', + }, + { + alpha2: 'KZ', alpha3: 'KAZ', code: 398, numericString: '398', name: 'Kazakhstan', + }, + { + alpha2: 'KE', alpha3: 'KEN', code: 404, numericString: '404', name: 'Kenya', + }, + { + alpha2: 'KI', alpha3: 'KIR', code: 296, numericString: '296', name: 'Kiribati', + }, + { + alpha2: 'KP', alpha3: 'PRK', code: 408, numericString: '408', name: 'Korea, Democratic People\'s Republic of', + }, + { + alpha2: 'KR', alpha3: 'KOR', code: 410, numericString: '410', name: 'Korea, Republic of', + }, + { + alpha2: 'KW', alpha3: 'KWT', code: 414, numericString: '414', name: 'Kuwait', + }, + { + alpha2: 'KG', alpha3: 'KGZ', code: 417, numericString: '417', name: 'Kyrgyzstan', + }, + { + alpha2: 'LA', alpha3: 'LAO', code: 418, numericString: '418', name: 'Lao People\'s Democratic Republic', + }, + { + alpha2: 'LV', alpha3: 'LVA', code: 428, numericString: '428', name: 'Latvia', + }, + { + alpha2: 'LB', alpha3: 'LBN', code: 422, numericString: '422', name: 'Lebanon', + }, + { + alpha2: 'LS', alpha3: 'LSO', code: 426, numericString: '426', name: 'Lesotho', + }, + { + alpha2: 'LR', alpha3: 'LBR', code: 430, numericString: '430', name: 'Liberia', + }, + { + alpha2: 'LY', alpha3: 'LBY', code: 434, numericString: '434', name: 'Libya', + }, + { + alpha2: 'LI', alpha3: 'LIE', code: 438, numericString: '438', name: 'Liechtenstein', + }, + { + alpha2: 'LT', alpha3: 'LTU', code: 440, numericString: '440', name: 'Lithuania', + }, + { + alpha2: 'LU', alpha3: 'LUX', code: 442, numericString: '442', name: 'Luxembourg', + }, + { + alpha2: 'MO', alpha3: 'MAC', code: 446, numericString: '446', name: 'Macao', + }, + { + alpha2: 'MK', alpha3: 'MKD', code: 807, numericString: '807', name: 'Macedonia, the former Yugoslav Republic of', + }, + { + alpha2: 'MG', alpha3: 'MDG', code: 450, numericString: '450', name: 'Madagascar', + }, + { + alpha2: 'MW', alpha3: 'MWI', code: 454, numericString: '454', name: 'Malawi', + }, + { + alpha2: 'MY', alpha3: 'MYS', code: 458, numericString: '458', name: 'Malaysia', + }, + { + alpha2: 'MV', alpha3: 'MDV', code: 462, numericString: '462', name: 'Maldives', + }, + { + alpha2: 'ML', alpha3: 'MLI', code: 466, numericString: '466', name: 'Mali', + }, + { + alpha2: 'MT', alpha3: 'MLT', code: 470, numericString: '470', name: 'Malta', + }, + { + alpha2: 'MH', alpha3: 'MHL', code: 584, numericString: '584', name: 'Marshall Islands', + }, + { + alpha2: 'MQ', alpha3: 'MTQ', code: 474, numericString: '474', name: 'Martinique', + }, + { + alpha2: 'MR', alpha3: 'MRT', code: 478, numericString: '478', name: 'Mauritania', + }, + { + alpha2: 'MU', alpha3: 'MUS', code: 480, numericString: '480', name: 'Mauritius', + }, + { + alpha2: 'YT', alpha3: 'MYT', code: 175, numericString: '175', name: 'Mayotte', + }, + { + alpha2: 'MX', alpha3: 'MEX', code: 484, numericString: '484', name: 'Mexico', + }, + { + alpha2: 'FM', alpha3: 'FSM', code: 583, numericString: '583', name: 'Micronesia, Federated States of', + }, + { + alpha2: 'MD', alpha3: 'MDA', code: 498, numericString: '498', name: 'Moldova, Republic of', + }, + { + alpha2: 'MC', alpha3: 'MCO', code: 492, numericString: '492', name: 'Monaco', + }, + { + alpha2: 'MN', alpha3: 'MNG', code: 496, numericString: '496', name: 'Mongolia', + }, + { + alpha2: 'ME', alpha3: 'MNE', code: 499, numericString: '499', name: 'Montenegro', + }, + { + alpha2: 'MS', alpha3: 'MSR', code: 500, numericString: '500', name: 'Montserrat', + }, + { + alpha2: 'MA', alpha3: 'MAR', code: 504, numericString: '504', name: 'Morocco', + }, + { + alpha2: 'MZ', alpha3: 'MOZ', code: 508, numericString: '508', name: 'Mozambique', + }, + { + alpha2: 'MM', alpha3: 'MMR', code: 104, numericString: '104', name: 'Myanmar', + }, + { + alpha2: 'NA', alpha3: 'NAM', code: 516, numericString: '516', name: 'Namibia', + }, + { + alpha2: 'NR', alpha3: 'NRU', code: 520, numericString: '520', name: 'Nauru', + }, + { + alpha2: 'NP', alpha3: 'NPL', code: 524, numericString: '524', name: 'Nepal', + }, + { + alpha2: 'NL', alpha3: 'NLD', code: 528, numericString: '528', name: 'Netherlands', + }, + { + alpha2: 'NC', alpha3: 'NCL', code: 540, numericString: '540', name: 'New Caledonia', + }, + { + alpha2: 'NZ', alpha3: 'NZL', code: 554, numericString: '554', name: 'New Zealand', + }, + { + alpha2: 'NI', alpha3: 'NIC', code: 558, numericString: '558', name: 'Nicaragua', + }, + { + alpha2: 'NE', alpha3: 'NER', code: 562, numericString: '562', name: 'Niger', + }, + { + alpha2: 'NG', alpha3: 'NGA', code: 566, numericString: '566', name: 'Nigeria', + }, + { + alpha2: 'NU', alpha3: 'NIU', code: 570, numericString: '570', name: 'Niue', + }, + { + alpha2: 'NF', alpha3: 'NFK', code: 574, numericString: '574', name: 'Norfolk Island', + }, + { + alpha2: 'MP', alpha3: 'MNP', code: 580, numericString: '580', name: 'Northern Mariana Islands', + }, + { + alpha2: 'NO', alpha3: 'NOR', code: 578, numericString: '578', name: 'Norway', + }, + { + alpha2: 'OM', alpha3: 'OMN', code: 512, numericString: '512', name: 'Oman', + }, + { + alpha2: 'PK', alpha3: 'PAK', code: 586, numericString: '586', name: 'Pakistan', + }, + { + alpha2: 'PW', alpha3: 'PLW', code: 585, numericString: '585', name: 'Palau', + }, + { + alpha2: 'PS', alpha3: 'PSE', code: 275, numericString: '275', name: 'Palestine, State of', + }, + { + alpha2: 'PA', alpha3: 'PAN', code: 591, numericString: '591', name: 'Panama', + }, + { + alpha2: 'PG', alpha3: 'PNG', code: 598, numericString: '598', name: 'Papua New Guinea', + }, + { + alpha2: 'PY', alpha3: 'PRY', code: 600, numericString: '600', name: 'Paraguay', + }, + { + alpha2: 'PE', alpha3: 'PER', code: 604, numericString: '604', name: 'Peru', + }, + { + alpha2: 'PH', alpha3: 'PHL', code: 608, numericString: '608', name: 'Philippines', + }, + { + alpha2: 'PN', alpha3: 'PCN', code: 612, numericString: '612', name: 'Pitcairn', + }, + { + alpha2: 'PL', alpha3: 'POL', code: 616, numericString: '616', name: 'Poland', + }, + { + alpha2: 'PT', alpha3: 'PRT', code: 620, numericString: '620', name: 'Portugal', + }, + { + alpha2: 'PR', alpha3: 'PRI', code: 630, numericString: '630', name: 'Puerto Rico', + }, + { + alpha2: 'QA', alpha3: 'QAT', code: 634, numericString: '634', name: 'Qatar', + }, + { + alpha2: 'RE', alpha3: 'REU', code: 638, numericString: '638', name: 'Réunion', + }, + { + alpha2: 'RO', alpha3: 'ROU', code: 642, numericString: '642', name: 'Romania', + }, + { + alpha2: 'RU', alpha3: 'RUS', code: 643, numericString: '643', name: 'Russian Federation', + }, + { + alpha2: 'RW', alpha3: 'RWA', code: 646, numericString: '646', name: 'Rwanda', + }, + { + alpha2: 'BL', alpha3: 'BLM', code: 652, numericString: '652', name: 'Saint Barthélemy', + }, + { + alpha2: 'SH', alpha3: 'SHN', code: 654, numericString: '654', name: 'Saint Helena, Ascension and Tristan da Cunha', + }, + { + alpha2: 'KN', alpha3: 'KNA', code: 659, numericString: '659', name: 'Saint Kitts and Nevis', + }, + { + alpha2: 'LC', alpha3: 'LCA', code: 662, numericString: '662', name: 'Saint Lucia', + }, + { + alpha2: 'MF', alpha3: 'MAF', code: 663, numericString: '663', name: 'Saint Martin (French part)', + }, + { + alpha2: 'PM', alpha3: 'SPM', code: 666, numericString: '666', name: 'Saint Pierre and Miquelon', + }, + { + alpha2: 'VC', alpha3: 'VCT', code: 670, numericString: '670', name: 'Saint Vincent and the Grenadines', + }, + { + alpha2: 'WS', alpha3: 'WSM', code: 882, numericString: '882', name: 'Samoa', + }, + { + alpha2: 'SM', alpha3: 'SMR', code: 674, numericString: '674', name: 'San Marino', + }, + { + alpha2: 'ST', alpha3: 'STP', code: 678, numericString: '678', name: 'Sao Tome and Principe', + }, + { + alpha2: 'SA', alpha3: 'SAU', code: 682, numericString: '682', name: 'Saudi Arabia', + }, + { + alpha2: 'SN', alpha3: 'SEN', code: 686, numericString: '686', name: 'Senegal', + }, + { + alpha2: 'RS', alpha3: 'SRB', code: 688, numericString: '688', name: 'Serbia', + }, + { + alpha2: 'SC', alpha3: 'SYC', code: 690, numericString: '690', name: 'Seychelles', + }, + { + alpha2: 'SL', alpha3: 'SLE', code: 694, numericString: '694', name: 'Sierra Leone', + }, + { + alpha2: 'SG', alpha3: 'SGP', code: 702, numericString: '702', name: 'Singapore', + }, + { + alpha2: 'SX', alpha3: 'SXM', code: 534, numericString: '534', name: 'Sint Maarten (Dutch part)', + }, + { + alpha2: 'SK', alpha3: 'SVK', code: 703, numericString: '703', name: 'Slovakia', + }, + { + alpha2: 'SI', alpha3: 'SVN', code: 705, numericString: '705', name: 'Slovenia', + }, + { + alpha2: 'SB', alpha3: 'SLB', code: 90, numericString: '090', name: 'Solomon Islands', + }, + { + alpha2: 'SO', alpha3: 'SOM', code: 706, numericString: '706', name: 'Somalia', + }, + { + alpha2: 'ZA', alpha3: 'ZAF', code: 710, numericString: '710', name: 'South Africa', + }, + { + alpha2: 'GS', alpha3: 'SGS', code: 239, numericString: '239', name: 'South Georgia and the South Sandwich Islands', + }, + { + alpha2: 'SS', alpha3: 'SSD', code: 728, numericString: '728', name: 'South Sudan', + }, + { + alpha2: 'ES', alpha3: 'ESP', code: 724, numericString: '724', name: 'Spain', + }, + { + alpha2: 'LK', alpha3: 'LKA', code: 144, numericString: '144', name: 'Sri Lanka', + }, + { + alpha2: 'SD', alpha3: 'SDN', code: 729, numericString: '729', name: 'Sudan', + }, + { + alpha2: 'SR', alpha3: 'SUR', code: 740, numericString: '740', name: 'Suriname', + }, + { + alpha2: 'SJ', alpha3: 'SJM', code: 744, numericString: '744', name: 'Svalbard and Jan Mayen', + }, + { + alpha2: 'SZ', alpha3: 'SWZ', code: 748, numericString: '748', name: 'Swaziland', + }, + { + alpha2: 'SE', alpha3: 'SWE', code: 752, numericString: '752', name: 'Sweden', + }, + { + alpha2: 'CH', alpha3: 'CHE', code: 756, numericString: '756', name: 'Switzerland', + }, + { + alpha2: 'SY', alpha3: 'SYR', code: 760, numericString: '760', name: 'Syrian Arab Republic', + }, + { + alpha2: 'TW', alpha3: 'TWN', code: 158, numericString: '158', name: 'Taiwan', + }, + { + alpha2: 'TJ', alpha3: 'TJK', code: 762, numericString: '762', name: 'Tajikistan', + }, + { + alpha2: 'TZ', alpha3: 'TZA', code: 834, numericString: '834', name: 'Tanzania, United Republic of', + }, + { + alpha2: 'TH', alpha3: 'THA', code: 764, numericString: '764', name: 'Thailand', + }, + { + alpha2: 'TL', alpha3: 'TLS', code: 626, numericString: '626', name: 'Timor-Leste', + }, + { + alpha2: 'TG', alpha3: 'TGO', code: 768, numericString: '768', name: 'Togo', + }, + { + alpha2: 'TK', alpha3: 'TKL', code: 772, numericString: '772', name: 'Tokelau', + }, + { + alpha2: 'TO', alpha3: 'TON', code: 776, numericString: '776', name: 'Tonga', + }, + { + alpha2: 'TT', alpha3: 'TTO', code: 780, numericString: '780', name: 'Trinidad and Tobago', + }, + { + alpha2: 'TN', alpha3: 'TUN', code: 788, numericString: '788', name: 'Tunisia', + }, + { + alpha2: 'TR', alpha3: 'TUR', code: 792, numericString: '792', name: 'Turkey', + }, + { + alpha2: 'TM', alpha3: 'TKM', code: 795, numericString: '795', name: 'Turkmenistan', + }, + { + alpha2: 'TC', alpha3: 'TCA', code: 796, numericString: '796', name: 'Turks and Caicos Islands', + }, + { + alpha2: 'TV', alpha3: 'TUV', code: 798, numericString: '798', name: 'Tuvalu', + }, + { + alpha2: 'UG', alpha3: 'UGA', code: 800, numericString: '800', name: 'Uganda', + }, + { + alpha2: 'UA', alpha3: 'UKR', code: 804, numericString: '804', name: 'Ukraine', + }, + { + alpha2: 'AE', alpha3: 'ARE', code: 784, numericString: '784', name: 'United Arab Emirates', + }, + { + alpha2: 'GB', alpha3: 'GBR', code: 826, numericString: '826', name: 'United Kingdom', + }, + { + alpha2: 'US', alpha3: 'USA', code: 840, numericString: '840', name: 'United States', + }, + { + alpha2: 'UM', alpha3: 'UMI', code: 581, numericString: '581', name: 'United States Minor Outlying Islands', + }, + { + alpha2: 'UY', alpha3: 'URY', code: 858, numericString: '858', name: 'Uruguay', + }, + { + alpha2: 'UZ', alpha3: 'UZB', code: 860, numericString: '860', name: 'Uzbekistan', + }, + { + alpha2: 'VU', alpha3: 'VUT', code: 548, numericString: '548', name: 'Vanuatu', + }, + { + alpha2: 'VE', alpha3: 'VEN', code: 862, numericString: '862', name: 'Venezuela, Bolivarian Republic of', + }, + { + alpha2: 'VN', alpha3: 'VNM', code: 704, numericString: '704', name: 'Viet Nam', + }, + { + alpha2: 'VG', alpha3: 'VGB', code: 92, numericString: '092', name: 'Virgin Islands, British', + }, + { + alpha2: 'VI', alpha3: 'VIR', code: 850, numericString: '850', name: 'Virgin Islands, U.S.', + }, + { + alpha2: 'WF', alpha3: 'WLF', code: 876, numericString: '876', name: 'Wallis and Futuna', + }, + { + alpha2: 'EH', alpha3: 'ESH', code: 732, numericString: '732', name: 'Western Sahara', + }, + { + alpha2: 'YE', alpha3: 'YEM', code: 887, numericString: '887', name: 'Yemen', + }, + { + alpha2: 'ZM', alpha3: 'ZMB', code: 894, numericString: '894', name: 'Zambia', + }, + { + alpha2: 'ZW', alpha3: 'ZWE', code: 716, numericString: '716', name: 'Zimbabwe', + }, +]; + +export default ISOCountries; diff --git a/src/shared/components/MemberSearch/helpers/index.js b/src/shared/components/MemberSearch/helpers/index.js new file mode 100644 index 0000000000..a5adcec034 --- /dev/null +++ b/src/shared/components/MemberSearch/helpers/index.js @@ -0,0 +1,246 @@ +import _ from 'lodash'; + +// Member Levels +export function memberLevelByRating(userRating) { + const levelRatings = [0, 900, 1200, 1500, 2200]; + + const userLevel = _.findLastIndex(levelRatings, (rating) => { + if (userRating >= rating) { + return true; + } + return false; + }); + + if (userLevel === -1) return 1; + + return userLevel + 1; +} + +export function memberColorByLevel(userLevel) { + const colorsByLevel = { + 1: '#A3A3AD', + 2: '#25C089', + 3: '#666EFF', + 4: '#FCB816', + 5: '#E6175C', + }; + + const color = colorsByLevel[userLevel] || colorsByLevel[1]; + + return color; +} + +// Process member skills +export function sortSkillsByScoreAndTag(skills, tag, numSkillsToReturn = Infinity) { + if (_.isEmpty(skills)) return []; + + const sortedSkills = _.orderBy(skills, 'score', 'desc'); + + // If the user has the tag, move it to the front + if (tag) { + const tagIndex = _.findIndex(sortedSkills, skill => skill.name === tag.name); + + if (tagIndex !== -1) { + const tagSkill = sortedSkills.splice(tagIndex, 1)[0]; + tagSkill.searchedTag = true; + + sortedSkills.unshift(tagSkill); + } + } + + return sortedSkills.slice(0, numSkillsToReturn); +} + +// Subtrack Abbreviations +export function getSubtrackAbbreviation(subtrack) { + const subtrackAbbreviations = { + APPLICATION_FRONT_END_DESIGN: 'FE', + ARCHITECTURE: 'Ar', + ASSEMBLY_COMPETITION: 'As', + BANNERS_OR_ICONS: 'BI', + BUG_HUNT: 'BH', + CODE: 'Cd', + COMPONENT_PRODUCTION: 'Cp', + CONCEPTUALIZATION: 'Cn', + CONTENT_CREATION: 'CC', + COPILOT: 'FS', + COPILOT_POSTING: 'CP', + DEPLOYMENT: 'Dp', + DESIGN: 'Ds', + DESIGN_FIRST_2_FINISH: 'F2F', + DEVELOP_MARATHON_MATCH: 'MM', + DEVELOPMENT: 'Dv', + FIRST_2_FINISH: 'F2F', + FRONT_END_FLASH: 'Fl', + GENERIC_SCORECARDS: 'G', + IDEA_GENERATION: 'IG', + LOGO_DESIGN: 'Lg', + MARATHON_MATCH: 'MM', + PRINT_OR_PRESENTATION: 'PP', + PROCESS: 'Ps', + REPORTING: 'Rp', + RIA_BUILD_COMPETITION: 'RB', + RIA_COMPONENT_COMPETITION: 'RC', + SECURITY: 'Sc', + SPEC_REVIEW: 'SR', + SPECIFICATION: 'SPC', + SRM: 'SRM', + STUDIO_OTHER: 'O', + TEST_SCENARIOS: 'Ts', + TEST_SUITES: 'TS', + TESTING_COMPETITION: 'Tg', + UI_PROTOTYPE_COMPETITION: 'Pr', + WEB_DESIGNS: 'Wb', + WIDGET_OR_MOBILE_SCREEN_DESIGN: 'Wg', + WIREFRAMES: 'Wf', + }; + + const abbreviation = subtrackAbbreviations[subtrack] || 'O'; + + return abbreviation; +} + +export function getSubtrackStat(subtrackStats) { + if (subtrackStats.fulfillment) { + return { + value: subtrackStats.fulfillment, + type: 'fulfillment', + }; + } + + const subtrackRating = _.get(subtrackStats, 'rank.rating'); + + if (subtrackRating) { + return { + value: subtrackRating, + type: 'rating', + }; + } + + return { + value: subtrackStats.wins || 0, + type: 'wins', + }; +} + +// Subtrack filtering +export function getMostRecentSubtracks(userStatsByTrack, numResults = Infinity) { + let subtrackStats = []; + + // If a user is a copilot with > 10 challenges and > 80% fulfillment, + // add it to the list of subtracks + const hasQualifyingFulfillment = _.get(userStatsByTrack, 'COPILOT.fulfillment', 0) >= 80; + const hasQualifyingNumChallenges = _.get(userStatsByTrack, 'COPILOT.contests', 0) >= 10; + + if (hasQualifyingFulfillment && hasQualifyingNumChallenges) { + subtrackStats.push({ + track: 'COPILOT', + name: 'COPILOT', + stat: getSubtrackStat(userStatsByTrack.COPILOT), + }); + } + + // Process subtracks in Data Science + const marathonMatchStats = _.get(userStatsByTrack, 'DATA_SCIENCE.MARATHON_MATCH', {}); + const SRMStats = _.get(userStatsByTrack, 'DATA_SCIENCE.SRM', {}); + + if (marathonMatchStats.mostRecentEventDate) { + subtrackStats.push({ + track: 'DATA_SCIENCE', + name: 'MARATHON_MATCH', + mostRecentEventDate: marathonMatchStats.mostRecentEventDate, + stat: getSubtrackStat(marathonMatchStats), + }); + } + + if (SRMStats.mostRecentEventDate) { + subtrackStats.push({ + track: 'DATA_SCIENCE', + name: 'SRM', + mostRecentEventDate: SRMStats.mostRecentEventDate, + stat: getSubtrackStat(SRMStats), + }); + } + + // Process subtracks in Develop and Design + const designSubtracks = _.get(userStatsByTrack, 'DESIGN.subTracks', []); + const developSubtracks = _.get(userStatsByTrack, 'DEVELOP.subTracks', []); + + designSubtracks.forEach((subtrack) => { + if (subtrack.mostRecentEventDate) { + subtrackStats.push({ + track: 'DESIGN', + name: subtrack.name, + mostRecentEventDate: subtrack.mostRecentEventDate, + stat: getSubtrackStat(subtrack), + }); + } + }); + + developSubtracks.forEach((subtrack) => { + if (subtrack.mostRecentEventDate) { + subtrackStats.push({ + track: 'DEVELOP', + name: subtrack.name, + mostRecentEventDate: subtrack.mostRecentEventDate, + stat: getSubtrackStat(subtrack), + }); + } + }); + + // Filter out all subtracks with a value of 0 (wins, rating, etc.) + subtrackStats = subtrackStats.filter(stat => stat.stat.value !== 0); + + const sortedSubtracks = subtrackStats + .sort((a, b) => b.mostRecentEventDate - a.mostRecentEventDate); + + return sortedSubtracks.slice(0, numResults); +} + +// Detect end of the page on scroll +export function isEndOfScreen(callback, ...callbackArguments) { + if ((window.innerHeight + window.scrollY) >= document.body.offsetHeight - 400) { + callback(...callbackArguments); + } +} + +// Miscellaneous helpers +export function getRoundedPercentage(number) { + if (_.isFinite(number)) { + const roundedNumber = Math.round(number); + + return `${roundedNumber}%`; + } + + return ''; +} + +export function numberWithCommas(num) { + return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ','); +} + +export function singlePluralFormatter(num, noun) { + switch (num) { + case 0: + case undefined: + case null: + return ''; + case 1: + return `1 ${noun}`; + default: + return `${num} ${noun}s`; + } +} + +export function getSearchTagPreposition(tagType) { + switch (tagType.toUpperCase()) { + case 'SKILL': + return 'in'; + case 'COUNTRY': + return 'from'; + case 'EVENT': + return 'at the'; + default: + return 'in'; + } +} diff --git a/src/shared/components/MemberSearch/icons/LevelDesignatorIcon.jsx b/src/shared/components/MemberSearch/icons/LevelDesignatorIcon.jsx new file mode 100644 index 0000000000..3dc6d6dafc --- /dev/null +++ b/src/shared/components/MemberSearch/icons/LevelDesignatorIcon.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { memberColorByLevel } from '../helpers'; + +const LevelDesignatorIcon = ({ width, height, level }) => { + const fill = memberColorByLevel(level); + + return ( + + + + + + + + + + + ); +}; + +LevelDesignatorIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, + level: PropTypes.number.isRequired, +}; + +LevelDesignatorIcon.defaultProps = { + width: 0, + height: 0, +}; + +export default LevelDesignatorIcon; diff --git a/src/shared/components/MemberSearch/icons/RobotIcon.jsx b/src/shared/components/MemberSearch/icons/RobotIcon.jsx new file mode 100644 index 0000000000..6b2a9cf7c1 --- /dev/null +++ b/src/shared/components/MemberSearch/icons/RobotIcon.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const RobotIcon = ({ width, height }) => ( + + + + + + + + + + + + + + + + + + + + + +); + +RobotIcon.propTypes = { + width: PropTypes.number, + height: PropTypes.number, +}; + +RobotIcon.defaultProps = { + width: 0, + height: 0, +}; + +export default RobotIcon; diff --git a/src/shared/components/MemberSearch/icons/TrophyIcon.jsx b/src/shared/components/MemberSearch/icons/TrophyIcon.jsx new file mode 100644 index 0000000000..b3f51c831c --- /dev/null +++ b/src/shared/components/MemberSearch/icons/TrophyIcon.jsx @@ -0,0 +1,22 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const TrophyIcon = ({ fill }) => ( + + + + + + + +); + +TrophyIcon.propTypes = { + fill: PropTypes.string, +}; + +TrophyIcon.defaultProps = { + fill: '', +}; + +export default TrophyIcon; diff --git a/src/shared/components/MemberSearch/index.jsx b/src/shared/components/MemberSearch/index.jsx new file mode 100644 index 0000000000..9533de1548 --- /dev/null +++ b/src/shared/components/MemberSearch/index.jsx @@ -0,0 +1,70 @@ +import React, { Component } from 'react'; +import PropTypes from 'prop-types'; +import qs from 'qs'; +import MemberSearchView from './MemberSearchView'; +import { isEndOfScreen } from './helpers'; + +import './style.scss'; + +export default class MemberSearch extends Component { + constructor(props) { + super(props); + this.handleScroll = this.handleScroll.bind(this); + } + + componentWillMount() { + const { location, loadMemberSearch } = this.props; + window.addEventListener('scroll', this.handleScroll); + + this.searchTermFromQuery = qs.parse(location.search, { ignoreQueryPrefix: true }).q || ''; + loadMemberSearch(this.searchTermFromQuery); + } + + componentWillUnmount() { + window.removeEventListener('scroll', this.handleScroll); + } + + handleScroll() { + const { + moreMatchesAvailable, usernameMatches, loadingMore, pageLoaded, loadMemberSearch, + } = this.props; + + if (pageLoaded && !loadingMore && moreMatchesAvailable && usernameMatches.length > 10) { + isEndOfScreen(loadMemberSearch, this.searchTermFromQuery); + } + } + + render() { + return ( +
    +
    +
    +
    + +
    +
    +
    +
    + ); + } +} + +MemberSearch.propTypes = { + location: PropTypes.shape({ + search: PropTypes.string, + }).isRequired, + + pageLoaded: PropTypes.bool.isRequired, + loadingMore: PropTypes.bool.isRequired, + error: PropTypes.bool.isRequired, + + usernameMatches: PropTypes.arrayOf({}).isRequired, + moreMatchesAvailable: PropTypes.bool.isRequired, + totalCount: PropTypes.number.isRequired, + topMembers: PropTypes.shape([]).isRequired, + + previousSearchTerm: PropTypes.string.isRequired, + searchTermTag: PropTypes.shape({}).isRequired, + + loadMemberSearch: PropTypes.func.isRequired, +}; diff --git a/src/shared/components/MemberSearch/style.scss b/src/shared/components/MemberSearch/style.scss new file mode 100644 index 0000000000..1286420505 --- /dev/null +++ b/src/shared/components/MemberSearch/style.scss @@ -0,0 +1,43 @@ +.page-wrapper { + display: block !important; // overrides .container>*:nth-child(2) + width: 100%; + min-height: 100%; + background-color: #f6f6f6; +} + +.fold-wrapper { + margin: 0; + min-height: 100%; +} + +.view-container { + min-height: 440px; + padding-bottom: 30px; +} + +:global { + #member-search-wrapper { + font-family: "Roboto", Helvetica, Arial, sans-serif !important; + + // Overrides .member-search-view + > div { + background-color: transparent; + } + + // Overrides old style guide + a { + cursor: auto !important; + transition: none !important; + text-decoration: none !important; + color: inherit !important; + + &:visited, + &:hover, + &:active { + color: inherit !important; + cursor: auto !important; + text-decoration: none !important; + } + } + } +} diff --git a/src/shared/containers/MemberSearch.jsx b/src/shared/containers/MemberSearch.jsx new file mode 100644 index 0000000000..b05f7d0d95 --- /dev/null +++ b/src/shared/containers/MemberSearch.jsx @@ -0,0 +1,85 @@ +import _ from 'lodash'; +import { connect } from 'react-redux'; +import { actions } from 'topcoder-react-lib'; +import MemberSearch from 'components/MemberSearch'; + +function mapStateToProps({ memberSearch }) { + return { + pageLoaded: memberSearch.pageLoaded, + loadingMore: memberSearch.loadingMore, + error: memberSearch.error, + + usernameMatches: memberSearch.usernameMatches, + moreMatchesAvailable: memberSearch.moreMatchesAvailable, + totalCount: memberSearch.totalCount, + topMembers: memberSearch.topMembers, + + previousSearchTerm: memberSearch.previousSearchTerm, + searchTermTag: memberSearch.searchTermTag, + }; +} + +function mapDispatchToProps(dispatch) { + const memberSearchActions = actions.memberSearch; + return { + loadMemberSearch: (searchTerm, stateProps) => { + const numCurrentUsernameMatches = stateProps.usernameMatches.length; + const { previousSearchTerm } = stateProps; + const isPreviousSearchTerm = _.isString(previousSearchTerm); + const isNewSearchTerm = isPreviousSearchTerm && searchTerm.toLowerCase() + !== previousSearchTerm.toLowerCase(); + + if (isNewSearchTerm) { + dispatch(memberSearchActions.clearMemberSearch()); + } else if (previousSearchTerm && numCurrentUsernameMatches >= 10) { + dispatch(memberSearchActions.loadMoreUsernames()); + dispatch(memberSearchActions.usernameSearchSuccess(searchTerm, numCurrentUsernameMatches)); + return; + } + + dispatch(memberSearchActions.setSearchTerm(searchTerm)); + dispatch(memberSearchActions.checkIfSearchTermIsATag(searchTerm)) + .then((res) => { + const tag = res.payload; + dispatch(memberSearchActions.setSearchTag(tag)); + + const topMemberSearchSuccessAction = tag + ? memberSearchActions.topMemberSearchSuccess(tag) : null; + const usernameSearchSuccessAction = memberSearchActions + .usernameSearchSuccess(searchTerm); + + let p; + if (topMemberSearchSuccessAction) { + p = dispatch(topMemberSearchSuccessAction) + .then(() => dispatch(usernameSearchSuccessAction)); + } else { + p = dispatch(usernameSearchSuccessAction); + } + + return p + .then(() => dispatch(memberSearchActions.memberSearchSuccess())) + .catch((err) => { + dispatch(memberSearchActions.resetSearchTerm()); + throw new Error(`Could not resolve all promises. Reason: ${err}`); + }); + }); + }, + }; +} + +function mergeProps(stateProps, dispatchProps, ownProps) { + return { + ...ownProps, + ...stateProps, + ...dispatchProps, + loadMemberSearch: searchTerm => dispatchProps.loadMemberSearch(searchTerm, stateProps), + }; +} + +const Container = connect( + mapStateToProps, + mapDispatchToProps, + mergeProps, +)(MemberSearch); + +export default Container; diff --git a/src/shared/routes/Topcoder/Routes.jsx b/src/shared/routes/Topcoder/Routes.jsx index a67e400321..2f7da40d98 100644 --- a/src/shared/routes/Topcoder/Routes.jsx +++ b/src/shared/routes/Topcoder/Routes.jsx @@ -32,6 +32,7 @@ import HallOfFame from '../HallOfFame'; import Profile from '../Profile'; import Scoreboard from '../tco/scoreboard'; import ProfileStats from '../ProfileStats'; +import MemberSearch from '../../containers/MemberSearch'; import './styles.scss'; @@ -139,6 +140,11 @@ export default function Topcoder() { exact path={`${config.TC_EDU_BASE_PATH}${config.TC_EDU_ARTICLES_PATH}/:articleTitle`} /> + )} From c75fa2d86809f478d5c9b76d26ac464208818132 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Tue, 25 Feb 2020 21:46:07 +0800 Subject: [PATCH 2/8] final fix --- .../MemberSearch/ListContainer/index.jsx | 2 +- .../MemberSearch/MemberItem/UserInfo/index.jsx | 13 +++++++++---- .../MemberItem/UserStats/index.jsx | 7 ++++--- .../MemberSearch/MemberList/index.jsx | 3 ++- .../MemberSearch/MemberSearchView/index.jsx | 15 +++++++++++---- .../MemberSearch/SubtrackList/index.jsx | 5 +++-- .../MemberSearch/TagList/TagItem/index.jsx | 4 ++-- .../components/MemberSearch/TagList/index.jsx | 5 +++-- .../MemberSearch/TopMemberList/index.jsx | 3 ++- .../MemberSearch/TrackList/index.jsx | 5 +++-- .../MemberSearch/User/UserAvatar/index.jsx | 6 +++++- .../MemberSearch/icons/LevelDesignatorIcon.jsx | 10 +++++----- .../MemberSearch/icons/RobotIcon.jsx | 8 ++++---- src/shared/components/MemberSearch/index.jsx | 18 +++++++++++++----- 14 files changed, 67 insertions(+), 37 deletions(-) diff --git a/src/shared/components/MemberSearch/ListContainer/index.jsx b/src/shared/components/MemberSearch/ListContainer/index.jsx index 90e452721c..6a8305eeb1 100644 --- a/src/shared/components/MemberSearch/ListContainer/index.jsx +++ b/src/shared/components/MemberSearch/ListContainer/index.jsx @@ -46,7 +46,7 @@ ListContainer.propTypes = { ListContainer.defaultProps = { headerHighlightedText: '', - numListItems: [], + numListItems: 0, }; export default ListContainer; diff --git a/src/shared/components/MemberSearch/MemberItem/UserInfo/index.jsx b/src/shared/components/MemberSearch/MemberItem/UserInfo/index.jsx index 3ad3860a42..42713f9a50 100644 --- a/src/shared/components/MemberSearch/MemberItem/UserInfo/index.jsx +++ b/src/shared/components/MemberSearch/MemberItem/UserInfo/index.jsx @@ -51,12 +51,17 @@ UserInfo.propTypes = { photoURL: PropTypes.string, handle: PropTypes.string, competitionCountryCode: PropTypes.string, - wins: PropTypes.oneOf([PropTypes.number, null, undefined]), - createdAt: PropTypes.string, + wins: PropTypes.number, + createdAt: PropTypes.number, description: PropTypes.string, }).isRequired, - userPlace: PropTypes.number.isRequired, - withBio: PropTypes.bool.isRequired, + userPlace: PropTypes.number, + withBio: PropTypes.bool, +}; + +UserInfo.defaultProps = { + userPlace: null, + withBio: false, }; export default UserInfo; diff --git a/src/shared/components/MemberSearch/MemberItem/UserStats/index.jsx b/src/shared/components/MemberSearch/MemberItem/UserStats/index.jsx index 54c5b781f2..a3f9ec4d32 100644 --- a/src/shared/components/MemberSearch/MemberItem/UserStats/index.jsx +++ b/src/shared/components/MemberSearch/MemberItem/UserStats/index.jsx @@ -39,15 +39,16 @@ const UserStats = ({ member, userPlace, searchTermTag }) => { UserStats.propTypes = { member: PropTypes.shape({ - skills: PropTypes.arrayOf({}), + skills: PropTypes.arrayOf(PropTypes.shape({})), tracks: PropTypes.arrayOf(PropTypes.string), - stats: PropTypes.shape({}), + stats: PropTypes.arrayOf(PropTypes.shape({})), }).isRequired, - userPlace: PropTypes.number.isRequired, + userPlace: PropTypes.number, searchTermTag: PropTypes.shape({}), }; UserStats.defaultProps = { + userPlace: null, searchTermTag: null, }; diff --git a/src/shared/components/MemberSearch/MemberList/index.jsx b/src/shared/components/MemberSearch/MemberList/index.jsx index 77c8d0f18c..7e12d2dadc 100644 --- a/src/shared/components/MemberSearch/MemberList/index.jsx +++ b/src/shared/components/MemberSearch/MemberList/index.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import shortId from 'shortid'; import MemberItem from '../MemberItem'; import './style.scss'; @@ -7,7 +8,7 @@ import './style.scss'; const MemberList = (({ members }) => (
    { - members.map(member => ) + members.map(member => ) }
    )); diff --git a/src/shared/components/MemberSearch/MemberSearchView/index.jsx b/src/shared/components/MemberSearch/MemberSearchView/index.jsx index 4a42cce6e3..801a692ed2 100644 --- a/src/shared/components/MemberSearch/MemberSearchView/index.jsx +++ b/src/shared/components/MemberSearch/MemberSearchView/index.jsx @@ -212,15 +212,22 @@ MemberSearchView.propTypes = { loadingMore: PropTypes.bool.isRequired, error: PropTypes.bool.isRequired, - usernameMatches: PropTypes.arrayOf({}).isRequired, + usernameMatches: PropTypes.arrayOf(PropTypes.shape({ + handle: PropTypes.string, + })).isRequired, moreMatchesAvailable: PropTypes.bool.isRequired, totalCount: PropTypes.number.isRequired, - topMembers: PropTypes.shape([]).isRequired, + topMembers: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - previousSearchTerm: PropTypes.string.isRequired, - searchTermTag: PropTypes.shape({}).isRequired, + previousSearchTerm: PropTypes.string, + searchTermTag: PropTypes.shape({}), loadMemberSearch: PropTypes.func.isRequired, }; +MemberSearchView.defaultProps = { + previousSearchTerm: null, + searchTermTag: null, +}; + export default MemberSearchView; diff --git a/src/shared/components/MemberSearch/SubtrackList/index.jsx b/src/shared/components/MemberSearch/SubtrackList/index.jsx index e3f994134d..a5f772fa1e 100644 --- a/src/shared/components/MemberSearch/SubtrackList/index.jsx +++ b/src/shared/components/MemberSearch/SubtrackList/index.jsx @@ -1,17 +1,18 @@ import React from 'react'; import PropTypes from 'prop-types'; +import shortId from 'shortid'; import SubtrackItem from './SubtrackItem'; import './style.scss'; const SubtrackList = ({ subtracks }) => (
    - {subtracks.map(s => )} + {subtracks.map(s => )}
    ); SubtrackList.propTypes = { - subtracks: PropTypes.shape([]).isRequired, + subtracks: PropTypes.arrayOf(PropTypes.shape({})).isRequired, }; export default SubtrackList; diff --git a/src/shared/components/MemberSearch/TagList/TagItem/index.jsx b/src/shared/components/MemberSearch/TagList/TagItem/index.jsx index ae8c8663a2..b33c2f57a5 100644 --- a/src/shared/components/MemberSearch/TagList/TagItem/index.jsx +++ b/src/shared/components/MemberSearch/TagList/TagItem/index.jsx @@ -20,8 +20,8 @@ const TagItem = ({ tag }) => { TagItem.propTypes = { tag: PropTypes.shape({ - searchedTag: PropTypes.string, - specialTag: PropTypes.string, + searchedTag: PropTypes.bool, + specialTag: PropTypes.bool, name: PropTypes.string, }).isRequired, }; diff --git a/src/shared/components/MemberSearch/TagList/index.jsx b/src/shared/components/MemberSearch/TagList/index.jsx index 426a175ab1..fe2cf33ff4 100644 --- a/src/shared/components/MemberSearch/TagList/index.jsx +++ b/src/shared/components/MemberSearch/TagList/index.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import classNames from 'classnames'; +import shortId from 'shortid'; import TagItem from './TagItem'; import './style.scss'; @@ -17,7 +18,7 @@ const TagList = ({ tags, label, emptyMessage = '' }) => { const noTagsMessage = !tags.length && emptyMessage ? emptyMessage : null; - const tagItems = tags.map(t => ); + const tagItems = tags.map(t => ); return (
    @@ -31,7 +32,7 @@ const TagList = ({ tags, label, emptyMessage = '' }) => { }; TagList.propTypes = { - tags: PropTypes.shape([]).isRequired, + tags: PropTypes.arrayOf(PropTypes.shape({})).isRequired, label: PropTypes.string, emptyMessage: PropTypes.string.isRequired, }; diff --git a/src/shared/components/MemberSearch/TopMemberList/index.jsx b/src/shared/components/MemberSearch/TopMemberList/index.jsx index 89039560f5..962bc631b2 100644 --- a/src/shared/components/MemberSearch/TopMemberList/index.jsx +++ b/src/shared/components/MemberSearch/TopMemberList/index.jsx @@ -1,13 +1,14 @@ import React from 'react'; import PropTypes from 'prop-types'; import _ from 'lodash'; +import shortId from 'shortid'; import MemberItem from '../MemberItem'; const TopMemberList = ({ topMembers }) => { const sortedTopMembers = _.orderBy(topMembers, 'wins', 'desc'); const topMemberItems = sortedTopMembers.map((member, i) => ( - + )); return ( diff --git a/src/shared/components/MemberSearch/TrackList/index.jsx b/src/shared/components/MemberSearch/TrackList/index.jsx index 0dd4ce1b47..5c52178e3c 100644 --- a/src/shared/components/MemberSearch/TrackList/index.jsx +++ b/src/shared/components/MemberSearch/TrackList/index.jsx @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import shortId from 'shortid'; import TrackItem from './TrackItem'; import './style.scss'; @@ -7,7 +8,7 @@ import './style.scss'; const TrackList = ({ tracks }) => { let trackItems; if (tracks.length) { - trackItems = tracks.map(t => ); + trackItems = tracks.map(t => ); } else { trackItems = ; } @@ -20,7 +21,7 @@ const TrackList = ({ tracks }) => { }; TrackList.propTypes = { - tracks: PropTypes.shape([]).isRequired, + tracks: PropTypes.arrayOf(PropTypes.string).isRequired, }; export default TrackList; diff --git a/src/shared/components/MemberSearch/User/UserAvatar/index.jsx b/src/shared/components/MemberSearch/User/UserAvatar/index.jsx index 2e1bf7601a..27add3e31b 100644 --- a/src/shared/components/MemberSearch/User/UserAvatar/index.jsx +++ b/src/shared/components/MemberSearch/User/UserAvatar/index.jsx @@ -9,7 +9,11 @@ const UserAvatar = ({ showLevel, rating, photoURL }) => { let levelIcon; if (showLevel) { - levelIcon = ; + levelIcon = ( + + + + ); } /* eslint-disable global-require */ diff --git a/src/shared/components/MemberSearch/icons/LevelDesignatorIcon.jsx b/src/shared/components/MemberSearch/icons/LevelDesignatorIcon.jsx index 3dc6d6dafc..01c77367f4 100644 --- a/src/shared/components/MemberSearch/icons/LevelDesignatorIcon.jsx +++ b/src/shared/components/MemberSearch/icons/LevelDesignatorIcon.jsx @@ -6,7 +6,7 @@ const LevelDesignatorIcon = ({ width, height, level }) => { const fill = memberColorByLevel(level); return ( - + @@ -20,14 +20,14 @@ const LevelDesignatorIcon = ({ width, height, level }) => { }; LevelDesignatorIcon.propTypes = { - width: PropTypes.number, - height: PropTypes.number, + width: PropTypes.string, + height: PropTypes.string, level: PropTypes.number.isRequired, }; LevelDesignatorIcon.defaultProps = { - width: 0, - height: 0, + width: '', + height: '', }; export default LevelDesignatorIcon; diff --git a/src/shared/components/MemberSearch/icons/RobotIcon.jsx b/src/shared/components/MemberSearch/icons/RobotIcon.jsx index 6b2a9cf7c1..2bc8801166 100644 --- a/src/shared/components/MemberSearch/icons/RobotIcon.jsx +++ b/src/shared/components/MemberSearch/icons/RobotIcon.jsx @@ -26,13 +26,13 @@ const RobotIcon = ({ width, height }) => ( ); RobotIcon.propTypes = { - width: PropTypes.number, - height: PropTypes.number, + width: PropTypes.string, + height: PropTypes.string, }; RobotIcon.defaultProps = { - width: 0, - height: 0, + width: '', + height: '', }; export default RobotIcon; diff --git a/src/shared/components/MemberSearch/index.jsx b/src/shared/components/MemberSearch/index.jsx index 9533de1548..108c17b78b 100644 --- a/src/shared/components/MemberSearch/index.jsx +++ b/src/shared/components/MemberSearch/index.jsx @@ -14,12 +14,15 @@ export default class MemberSearch extends Component { componentWillMount() { const { location, loadMemberSearch } = this.props; - window.addEventListener('scroll', this.handleScroll); this.searchTermFromQuery = qs.parse(location.search, { ignoreQueryPrefix: true }).q || ''; loadMemberSearch(this.searchTermFromQuery); } + componentDidMount() { + window.addEventListener('scroll', this.handleScroll); + } + componentWillUnmount() { window.removeEventListener('scroll', this.handleScroll); } @@ -58,13 +61,18 @@ MemberSearch.propTypes = { loadingMore: PropTypes.bool.isRequired, error: PropTypes.bool.isRequired, - usernameMatches: PropTypes.arrayOf({}).isRequired, + usernameMatches: PropTypes.arrayOf(PropTypes.shape({})).isRequired, moreMatchesAvailable: PropTypes.bool.isRequired, totalCount: PropTypes.number.isRequired, - topMembers: PropTypes.shape([]).isRequired, + topMembers: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - previousSearchTerm: PropTypes.string.isRequired, - searchTermTag: PropTypes.shape({}).isRequired, + previousSearchTerm: PropTypes.string, + searchTermTag: PropTypes.shape({}), loadMemberSearch: PropTypes.func.isRequired, }; + +MemberSearch.defaultProps = { + previousSearchTerm: null, + searchTermTag: null, +}; From ae95d6194d82145087bdfa11011430cef82841fd Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Tue, 3 Mar 2020 13:09:48 +0800 Subject: [PATCH 3/8] update topcoder-react-lib version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e87155e04..ffca2152b9 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "tc-accounts": "git+https://github.com/appirio-tech/accounts-app.git#dev", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", "tc-ui": "^1.0.12", - "topcoder-react-lib": "0.11.0", + "topcoder-react-lib": "1000.11.0", "topcoder-react-ui-kit": "^1.0.11", "topcoder-react-utils": "0.7.8", "turndown": "^4.0.2", From 46c6b0e12bac19d765b12bbd6f101c93fe923daa Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Tue, 3 Mar 2020 16:06:01 +0530 Subject: [PATCH 4/8] ci: deploying on beta --- .circleci/config.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6490d099a6..82375753dd 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -183,8 +183,7 @@ workflows: branches: only: - develop - - nav-hot-fix - - hot-fixes-leaderboard + - feature-member-search-list # This is beta env for production soft releases - "build-prod-beta": context : org-global From 287cab8b4465eb5f0650bd40f1aff79d8cedd01d Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Sat, 7 Mar 2020 12:58:13 +0800 Subject: [PATCH 5/8] update navigation-component lib --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ffca2152b9..7362684c87 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "moment-timezone": "^0.5.21", "money": "^0.2.0", "morgan": "^1.9.0", - "navigation-component": "git+https://github.com/topcoder-platform/navigation-component.git#develop", + "navigation-component": "git+https://github.com/topcoder-platform/navigation-component.git#feature-member-search-list", "node-forge": "^0.7.5", "nuka-carousel": "^4.5.3", "postcss": "^6.0.23", From d3fb986209d403063aa89a0c76252574089abd5a Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Sat, 7 Mar 2020 13:30:26 +0800 Subject: [PATCH 6/8] change depdency --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7362684c87..29748a40c9 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "moment-timezone": "^0.5.21", "money": "^0.2.0", "morgan": "^1.9.0", - "navigation-component": "git+https://github.com/topcoder-platform/navigation-component.git#feature-member-search-list", + "navigation-component": "git+https://github.com/topcoder-platform/navigation-component.git#v1000.1.4", "node-forge": "^0.7.5", "nuka-carousel": "^4.5.3", "postcss": "^6.0.23", From c926d8f11c044139540ae0ec4b628cbd3894a84e Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Sat, 7 Mar 2020 14:23:29 +0800 Subject: [PATCH 7/8] update version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 29748a40c9..d286f72a7c 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "moment-timezone": "^0.5.21", "money": "^0.2.0", "morgan": "^1.9.0", - "navigation-component": "git+https://github.com/topcoder-platform/navigation-component.git#v1000.1.4", + "navigation-component": "github:topcoder-platform/navigation-component.git#v1000.1.5", "node-forge": "^0.7.5", "nuka-carousel": "^4.5.3", "postcss": "^6.0.23", From 22dc2522f9242b8007131c65855c12b15f632477 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Sat, 7 Mar 2020 15:38:12 +0800 Subject: [PATCH 8/8] fix navigation issue --- package.json | 2 +- src/shared/components/MemberSearch/MemberItem/index.jsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index d286f72a7c..7362684c87 100644 --- a/package.json +++ b/package.json @@ -76,7 +76,7 @@ "moment-timezone": "^0.5.21", "money": "^0.2.0", "morgan": "^1.9.0", - "navigation-component": "github:topcoder-platform/navigation-component.git#v1000.1.5", + "navigation-component": "git+https://github.com/topcoder-platform/navigation-component.git#feature-member-search-list", "node-forge": "^0.7.5", "nuka-carousel": "^4.5.3", "postcss": "^6.0.23", diff --git a/src/shared/components/MemberSearch/MemberItem/index.jsx b/src/shared/components/MemberSearch/MemberItem/index.jsx index 976d617f38..0326e87241 100644 --- a/src/shared/components/MemberSearch/MemberItem/index.jsx +++ b/src/shared/components/MemberSearch/MemberItem/index.jsx @@ -2,7 +2,6 @@ import React from 'react'; import PropTypes from 'prop-types'; import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import classNames from 'classnames'; -import { config } from 'topcoder-react-utils'; import UserInfo from './UserInfo'; import UserStats from './UserStats'; @@ -22,7 +21,7 @@ const MemberItem = ({ const memberItem = (