diff --git a/.circleci/config.yml b/.circleci/config.yml index f73a20e51d..fa18367f07 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -231,6 +231,7 @@ workflows: branches: only: - develop + - feature-contentful # This is alternate dev env for parallel testing - "build-test": context : org-global @@ -252,6 +253,7 @@ workflows: branches: only: - develop + - feature-contentful # This is stage env for production QA releases - "build-prod-staging": context : org-global diff --git a/__tests__/shared/components/Contentful/Image/__snapshots__/Image.jsx.snap b/__tests__/shared/components/Contentful/Image/__snapshots__/Image.jsx.snap index b5a3e434fe..8540327616 100644 --- a/__tests__/shared/components/Contentful/Image/__snapshots__/Image.jsx.snap +++ b/__tests__/shared/components/Contentful/Image/__snapshots__/Image.jsx.snap @@ -3,6 +3,7 @@ exports[`Matches shallow shapshot 1`] = `
+
- THRIVE - +
`; diff --git a/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap b/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap index 8427f7e0c1..5ebefb6402 100644 --- a/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap +++ b/__tests__/shared/components/Leaderboard/__snapshots__/LeaderboardTable.jsx.snap @@ -44,16 +44,10 @@ exports[`Matches shallow shapshot 1`] = ` - @@ -97,16 +91,10 @@ exports[`Matches shallow shapshot 1`] = ` - @@ -150,16 +138,10 @@ exports[`Matches shallow shapshot 1`] = ` - @@ -203,16 +185,10 @@ exports[`Matches shallow shapshot 1`] = ` - diff --git a/__tests__/shared/components/Leaderboard/__snapshots__/PodiumSpot.jsx.snap b/__tests__/shared/components/Leaderboard/__snapshots__/PodiumSpot.jsx.snap index 9a000a3094..7d868e9f5a 100644 --- a/__tests__/shared/components/Leaderboard/__snapshots__/PodiumSpot.jsx.snap +++ b/__tests__/shared/components/Leaderboard/__snapshots__/PodiumSpot.jsx.snap @@ -7,16 +7,10 @@ exports[`Matches shallow shapshot 1`] = ` -
-
Avatar Photo \ No newline at end of file diff --git a/src/assets/images/icon-facebook.svg b/src/assets/images/icon-facebook.svg new file mode 100644 index 0000000000..ec2593813f --- /dev/null +++ b/src/assets/images/icon-facebook.svg @@ -0,0 +1,11 @@ + + + + 1EF461D5-8CE7-4936-91E5-80EBD141882D + Created with sketchtool. + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-linkedIn.svg b/src/assets/images/icon-linkedIn.svg new file mode 100644 index 0000000000..d7fbf67eaa --- /dev/null +++ b/src/assets/images/icon-linkedIn.svg @@ -0,0 +1,11 @@ + + + + A17EE70C-4FF2-418A-A9EE-80C7482C7231 + Created with sketchtool. + + + + + + \ No newline at end of file diff --git a/src/assets/images/icon-twitter.svg b/src/assets/images/icon-twitter.svg new file mode 100644 index 0000000000..d11f28d598 --- /dev/null +++ b/src/assets/images/icon-twitter.svg @@ -0,0 +1,11 @@ + + + + 98B36CB6-9363-4AE1-9293-675C31608E4A + Created with sketchtool. + + + + + + \ No newline at end of file diff --git a/src/assets/images/minimal-down.svg b/src/assets/images/minimal-down.svg new file mode 100644 index 0000000000..8a053c8f1e --- /dev/null +++ b/src/assets/images/minimal-down.svg @@ -0,0 +1,23 @@ + + + + C1A13778-3A8D-495F-8743-807086A83EFB + Created with sketchtool. + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/shared/actions/leaderboard.js b/src/shared/actions/leaderboard.js index 5fbc73a665..e1216d3301 100644 --- a/src/shared/actions/leaderboard.js +++ b/src/shared/actions/leaderboard.js @@ -42,7 +42,11 @@ async function getTcoHistoryChallengesDone(url, competitor) { const res = await fetch(url) .then(response => response.json()) .then(jsonResponse => ({ - challenges: _.filter(jsonResponse, challenge => challenge.userid === competitor.userid), + challenges: _.filter(jsonResponse, challenge => ( + challenge['tco_leaderboard.user_id'] + ? (challenge['tco_leaderboard.user_id'] === competitor['tco_leaderboard.user_id']) + : (challenge.userid === competitor.userid) + )), })); return res; } diff --git a/src/shared/components/Contentful/Article/Article.jsx b/src/shared/components/Contentful/Article/Article.jsx index 809f7c909c..150d213290 100644 --- a/src/shared/components/Contentful/Article/Article.jsx +++ b/src/shared/components/Contentful/Article/Article.jsx @@ -15,13 +15,14 @@ import LoadingIndicator from 'components/LoadingIndicator'; import YouTubeVideo from 'components/YouTubeVideo'; import moment from 'moment'; import localStorage from 'localStorage'; -import { config } from 'topcoder-react-utils'; -import ShareSocial from 'components/challenge-detail/Specification/SideBar/ShareSocial'; +import { config, Link, isomorphy } from 'topcoder-react-utils'; +import qs from 'qs'; // SVGs and assets import GestureIcon from 'assets/images/icon-gesture.svg'; -import UserDefault from 'assets/images/ico-user-default.svg'; import ReadMoreArrow from 'assets/images/read-more-arrow.svg'; -import qs from 'qs'; +import IconFacebook from 'assets/images/icon-facebook.svg'; +import IconTwitter from 'assets/images/icon-twitter.svg'; +import IconLinkedIn from 'assets/images/icon-linkedIn.svg'; const htmlToText = require('html-to-text'); @@ -105,20 +106,31 @@ export default class Article extends React.Component { spaceName, environment, preview, }; const { upvotes, downvotes } = this.state || {}; + let shareUrl; + if (isomorphy.isClientSide()) { + shareUrl = encodeURIComponent(window.location.href); + } return ( {/* Banner */} -
- { - fields.featuredImage ? ( -
- ) : null - } -
-
+ { + fields.featuredImage ? ( +
+ + + + + + + + +
+ ) : null + }
@@ -139,9 +151,7 @@ export default class Article extends React.Component { )} renderPlaceholder={LoadingIndicator} /> - ) : ( - - ) + ) : null }
@@ -169,14 +179,24 @@ export default class Article extends React.Component { { _.map(fields.tags, tag => (
- {tag} + {tag}
)) }

share

- +
{/* Content */} @@ -250,9 +270,9 @@ export default class Article extends React.Component { {subData.entries.items[rec.sys.id].fields.title} ) : ( - + {subData.entries.items[rec.sys.id].fields.title} - + ) } @@ -278,9 +298,9 @@ export default class Article extends React.Component { Read More ) : ( - + Read More - + ) }
diff --git a/src/shared/components/Contentful/Article/themes/default.scss b/src/shared/components/Contentful/Article/themes/default.scss index b823211fca..5b9b4b32a6 100644 --- a/src/shared/components/Contentful/Article/themes/default.scss +++ b/src/shared/components/Contentful/Article/themes/default.scss @@ -1,23 +1,22 @@ @import "~styles/mixins"; @import "~components/Contentful/default"; -.bannerBottomShape { - background-image: url(assets/images/wave.svg); - height: 120px; - background-repeat: no-repeat; - background-size: cover; - margin-top: -120px; - position: relative; +.shareButtons { + margin-top: 10px; - @include xs-to-sm { - height: 60px; - margin-top: -60px; + a { + margin-right: 5px; + + &:last-child { + margin-right: 0; + } } } -.contentContainer { +.contentContainer, +.contentContainerWithBanner { max-width: $screen-lg; - margin: 90px auto; + margin: 70px auto 90px auto; width: 100%; display: flex; @@ -67,13 +66,18 @@ } .name { - @include roboto-regular; + @include roboto-bold; margin-right: 6px; + color: #2a2a2a; + font-size: 14px; } .handle { - @include roboto-bold; + @include roboto-regular; + + color: #2a2a2a; + font-size: 14px; } } } @@ -411,17 +415,21 @@ } } +.contentContainerWithBanner { + margin-top: 0; +} + .bannerContainer { - background-image: linear-gradient(0deg, #06d6a0 0%, #63f963 100%); - position: relative; - min-height: 600px; + margin: auto; + max-width: 1024px; + width: 100%; - .featuredImage { - background-repeat: no-repeat; - background-size: cover; - background-position: center center; + @include xs-to-md { + padding: 0 15px; + } + + .site-header-background { width: 100%; - min-height: 600px; } } diff --git a/src/shared/components/Contentful/ArticleCard/ArticleCard.jsx b/src/shared/components/Contentful/ArticleCard/ArticleCard.jsx index d50230a139..38e99630a3 100644 --- a/src/shared/components/Contentful/ArticleCard/ArticleCard.jsx +++ b/src/shared/components/Contentful/ArticleCard/ArticleCard.jsx @@ -10,7 +10,7 @@ import { logger } from 'topcoder-react-lib'; import PT from 'prop-types'; import React from 'react'; import { themr } from 'react-css-super-themr'; -import { config } from 'topcoder-react-utils'; +import { config, Link } from 'topcoder-react-utils'; import markdown from 'utils/markdown'; import ReactDOMServer from 'react-dom/server'; // SVG assets @@ -29,7 +29,7 @@ const FORMAT = 'MMM DD, YYYY'; // date/time format for 'Forum post' cards is different from others const FORUM_POST_FORMAT = 'MMM DD, YYYY [at] h:mm A'; // max length for the title of the 'Article small' cards -const ART_SMALL_TITLE_MAX_LENGTH = 29; +const ART_SMALL_TITLE_MAX_LENGTH = 60; // Article large card 'breakpoint' const ARTICLE_LARGE_BREAKPOINT = 473; // character length for the content preview @@ -185,7 +185,7 @@ class ArticleCard extends React.Component { className={theme.tag} title={`Search for articles labelled as ${tag}`} > - {tag} + {tag} )) /* eslint-enable react/no-array-index-key */ @@ -195,13 +195,13 @@ class ArticleCard extends React.Component {

{article.readTime}

- {title} - +

{content}

@@ -226,14 +226,14 @@ class ArticleCard extends React.Component {
) } - {author.tcHandle} - +
)) ) : null @@ -253,13 +253,13 @@ class ArticleCard extends React.Component { {themeName === 'Article large' ?  .  : null} { contentAuthor && contentAuthor.length > 0 ? ( - {contentAuthor[0].name} - + ) : null }

@@ -290,13 +290,13 @@ class ArticleCard extends React.Component { }
) : ( - Read More - + ) }
diff --git a/src/shared/components/Contentful/ArticleCard/themes/article_small.scss b/src/shared/components/Contentful/ArticleCard/themes/article_small.scss index 7037ddff2d..b750b8ad1d 100644 --- a/src/shared/components/Contentful/ArticleCard/themes/article_small.scss +++ b/src/shared/components/Contentful/ArticleCard/themes/article_small.scss @@ -26,6 +26,9 @@ .main { flex-grow: 1; padding: 0 20px; + display: flex; + flex-direction: column; + justify-content: space-around; } .playIconContainer { @@ -68,15 +71,9 @@ color: #2a2a2a; font-family: 'Barlow', Helvetica, Arial, sans-serif; font-weight: 600; - font-size: 20px; - line-height: 22px; + font-size: 16px; + line-height: 20px; text-transform: uppercase; - letter-spacing: 0.3px; - - @include xs-to-sm { - font-size: 21px; - line-height: 23px; - } } .contentPreview { @@ -85,6 +82,9 @@ .infoContainer { margin: 11px 0 15px; + flex: 1; + display: flex; + align-items: flex-end; } .articleInfo { diff --git a/src/shared/components/Contentful/ContentBlock/themes/TCO19.scss b/src/shared/components/Contentful/ContentBlock/themes/TCO19.scss index 6dd6d736f7..737dff7cca 100644 --- a/src/shared/components/Contentful/ContentBlock/themes/TCO19.scss +++ b/src/shared/components/Contentful/ContentBlock/themes/TCO19.scss @@ -52,141 +52,7 @@ strong a { } @include gui-kit-headers; - - p { - @include tc-body-md; - - color: $tco-black; - font-size: 16px; - line-height: 26px; - margin-bottom: 20px; - margin-top: 0; - - strong { - @include roboto-medium; - - line-height: 20px; - text-align: left; - font-weight: 600; - margin-bottom: 20px; - } - } - - table { - margin-bottom: 20px; - - th { - @include roboto-regular; - - color: #2a2a2a; - font-size: 15px; - font-weight: bold; - line-height: 18px; - text-align: left; - text-transform: uppercase; - padding: 18px 10px 14px 0; - - @include md-to-xl { - white-space: nowrap; - } - - &:first-child { - padding-left: 10px; - } - } - - td { - @include roboto-regular; - - font-size: 15px; - line-height: 25px; - text-align: left; - color: $tc-gray-80; - border-top: 1px solid #d4d4d4; - border-bottom: 1px solid #d4d4d4; - padding: 20px 50px 20px 0; - min-height: 51px; - - &:first-child { - padding-left: 10px; - } - - &:last-child { - padding-right: 10px; - } - } - } - - ul, - ol { - @include tc-body-md; - - padding-left: 20px; - margin-bottom: 20px; - - @include roboto-regular; - - color: $tc-black; - - li p { - margin-bottom: 0; - } - } - - ul { - list-style-type: disc; - } - - ol { - list-style-type: decimal; - } - - img { - max-width: 100%; - border-radius: 6px; - } - - code { - @include roboto-mono-regular; - - color: #2a2a2a; - line-height: 1.5; - white-space: pre; - } - - sub, - sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; - } - - sup { - top: -0.5em; - } - - sub { - bottom: -0.25em; - } - - a { - @include roboto-regular; - - font-size: 16px; - line-height: 24px; - color: #0d61bf; - text-decoration: underline; - - &:hover { - text-decoration: none; - color: #0d61bf; - } - - &:visited { - color: #8231a9; - } - } + @include gui-kit-content; } .image { diff --git a/src/shared/components/Contentful/ContentBlock/themes/TCO20.scss b/src/shared/components/Contentful/ContentBlock/themes/TCO20.scss index e6b4a5a3e7..c54525c0c5 100644 --- a/src/shared/components/Contentful/ContentBlock/themes/TCO20.scss +++ b/src/shared/components/Contentful/ContentBlock/themes/TCO20.scss @@ -51,140 +51,7 @@ strong a { } @include gui-kit-headers; - - a { - @include roboto-regular; - - font-size: 16px; - line-height: 24px; - color: #0d61bf; - text-decoration: underline; - - &:hover { - text-decoration: none; - color: #0d61bf; - } - - &:visited { - color: #8231a9; - } - } - - p { - @include tc-body-md; - - color: $tco-black; - font-size: 16px; - line-height: 26px; - margin-bottom: 20px; - margin-top: 0; - - strong { - @include roboto-medium; - - line-height: 20px; - text-align: left; - font-weight: 600; - } - } - - table { - margin-bottom: 20px; - - th { - @include roboto-regular; - - color: #2a2a2a; - font-size: 15px; - font-weight: bold; - line-height: 18px; - text-align: left; - text-transform: uppercase; - padding: 18px 10px 14px 0; - - @include md-to-xl { - white-space: nowrap; - } - - &:first-child { - padding-left: 10px; - } - } - - td { - @include roboto-regular; - - font-size: 15px; - line-height: 25px; - text-align: left; - color: $tc-gray-80; - border-top: 1px solid #d4d4d4; - border-bottom: 1px solid #d4d4d4; - padding: 20px 50px 20px 0; - min-height: 51px; - - &:first-child { - padding-left: 10px; - } - - &:last-child { - padding-right: 10px; - } - } - } - - ul, - ol { - @include tc-body-md; - - padding-left: 20px; - margin-bottom: 20px; - - @include roboto-regular; - - color: $tc-black; - - li p { - margin-bottom: 0; - } - } - - ul { - list-style-type: disc; - } - - ol { - list-style-type: decimal; - } - - img { - max-width: 100%; - border-radius: 6px; - } - - code { - @include roboto-mono-regular; - - color: #2a2a2a; - line-height: 1.5; - white-space: pre; - } - - sub, - sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; - } - - sup { - top: -0.5em; - } - - sub { - bottom: -0.25em; - } + @include gui-kit-content; } .image { diff --git a/src/shared/components/Contentful/Dropdown/DropdownItem.jsx b/src/shared/components/Contentful/Dropdown/DropdownItem.jsx new file mode 100644 index 0000000000..1c7d36cee9 --- /dev/null +++ b/src/shared/components/Contentful/Dropdown/DropdownItem.jsx @@ -0,0 +1,68 @@ +/** + * Question and Answer Component + */ +/* eslint-disable react/no-danger */ +import React from 'react'; +import PT from 'prop-types'; +import MarkdownRenderer from 'components/MarkdownRenderer'; + +import './item.scss'; + +class DropdownItem extends React.Component { + constructor(props) { + super(props); + this.state = { + isActive: props.isActive, + }; + } + + toggleActive() { + const { isActive } = this.state; + this.setState({ + isActive: !isActive, + }); + } + + render() { + const { data } = this.props; + const { isActive } = this.state; + return ( +
+
(e.key === 'Enter' ? null : null)} + styleName={isActive ? 'question active' : 'question'} + onClick={() => this.toggleActive()} + > +
+ {data.fields.title} +
+
+
+
+ +
+
+ ); + } +} + +DropdownItem.defaultProps = { + preview: false, + spaceName: null, + environment: null, + isActive: false, +}; + +DropdownItem.propTypes = { + data: PT.shape().isRequired, + isActive: PT.bool, + preview: PT.bool, + spaceName: PT.string, + environment: PT.string, +}; + +export default DropdownItem; diff --git a/src/shared/components/Contentful/Dropdown/default.scss b/src/shared/components/Contentful/Dropdown/default.scss index 3be8c346bb..9022565a91 100644 --- a/src/shared/components/Contentful/Dropdown/default.scss +++ b/src/shared/components/Contentful/Dropdown/default.scss @@ -1,27 +1,18 @@ @import "~styles/mixins"; -.contentWrapper { - display: flex; - margin: 0 auto; - max-width: $screen-md; - padding: 15px; - color: black; +.container { + padding: 0; @include xs-to-sm { - flex-direction: column; + padding: 0 15px; } } -.container { - align-content: center; - background: white; - padding: 10px 0; - - &:nth-child(even) .contentWrapper { - flex-direction: row-reverse; - - @include xs-to-sm { - flex-direction: column; - } - } +.contentWrapper { + display: flex; + margin: 0 auto; + max-width: $screen-md; + color: black; + flex-direction: column; + border-top: 1px solid #d4d4d4; } diff --git a/src/shared/components/Contentful/Dropdown/index.jsx b/src/shared/components/Contentful/Dropdown/index.jsx index 95d9ea6c5a..92d6505fec 100644 --- a/src/shared/components/Contentful/Dropdown/index.jsx +++ b/src/shared/components/Contentful/Dropdown/index.jsx @@ -7,8 +7,8 @@ import ContentfulLoader from 'containers/ContentfulLoader'; import LoadingIndicator from 'components/LoadingIndicator'; import PT from 'prop-types'; import React from 'react'; -import FAQ from 'components/TrackHomePages/HowToCompetePage/FAQ'; import { fixStyle } from 'utils/contentful'; +import DropdownItem from './DropdownItem'; import defaultTheme from './default.scss'; @@ -27,7 +27,15 @@ function DropdownItemsLoader(props) { spaceName={spaceName} environment={environment} render={data => ( - item) }} hashLink="" /> + _.map(data.entries.items, item => ( + + )) )} renderPlaceholder={LoadingIndicator} /> diff --git a/src/shared/components/Contentful/Dropdown/item.scss b/src/shared/components/Contentful/Dropdown/item.scss new file mode 100644 index 0000000000..7cd898b901 --- /dev/null +++ b/src/shared/components/Contentful/Dropdown/item.scss @@ -0,0 +1,59 @@ +@import "~styles/mixins"; +@import "~components/Contentful/default"; + +.container { + width: 100%; +} + +.question { + display: flex; + align-items: center; + justify-content: space-between; + height: 82px; + outline: none; + cursor: pointer; + border-bottom: 1px solid #d4d4d4; + + &.active { + border-bottom: none; + } +} + +.answer { + @include gui-kit-content; + @include gui-kit-headers; + + display: none; + padding: 5px 70px 24px 0; + color: #2a2a2a; + + &.active { + display: block; + border-bottom: 1px solid #d4d4d4; + } +} + +.text { + color: #2a2a2a; + font-family: BarlowCondensed, sans-serif; + font-size: 31px; + letter-spacing: 0.2px; + text-transform: uppercase; + font-weight: 500; + + @include xs-to-sm { + max-width: 350px; + } +} + +.toggle-arrow { + background-image: url(assets/images/minimal-down.svg); + background-repeat: no-repeat; + align-self: right; + width: 24px; + height: 13px; + + &.active { + transform: scale(1, -1); + } +} diff --git a/src/shared/components/Contentful/Image/index.jsx b/src/shared/components/Contentful/Image/index.jsx index 333bf02fef..e24a1eb694 100644 --- a/src/shared/components/Contentful/Image/index.jsx +++ b/src/shared/components/Contentful/Image/index.jsx @@ -7,6 +7,7 @@ import LoadingIndicator from 'components/LoadingIndicator'; import PT from 'prop-types'; import React from 'react'; import _ from 'lodash'; +import { useMediaQuery } from 'react-responsive'; import Image from './Image'; import defaultTheme from './themes/default.scss'; @@ -17,6 +18,7 @@ export default function ImageLoader(props) { const { id, preview, spaceName, environment, } = props; + const isTabletOrMobile = useMediaQuery({ maxWidth: 768 }); return ( { const { fields } = data.entries.items[id]; - const imgId = _.get(fields, 'source.sys.id'); + const imgId = _.get(fields, isTabletOrMobile && fields.sourceMobile ? 'sourceMobile.sys.id' : 'source.sys.id'); const clipSvgId = _.get(fields, 'clipSvg.sys.id'); const assetIds = _.compact([imgId, clipSvgId]); const animationId = _.get(fields, 'animationOnScroll.sys.id'); diff --git a/src/shared/components/Contentful/Tabs/Tabs.jsx b/src/shared/components/Contentful/Tabs/Tabs.jsx index c4f790cc08..f107392203 100644 --- a/src/shared/components/Contentful/Tabs/Tabs.jsx +++ b/src/shared/components/Contentful/Tabs/Tabs.jsx @@ -17,11 +17,13 @@ import { TabPanel, } from 'react-tabs'; import { fixStyle } from 'utils/contentful'; +import { getQuery, updateQuery } from 'utils/url'; import defaultTheme from './themes/style.scss'; import zurichTheme from './themes/zurich.scss'; import tabsGroup from './themes/tabsGroup.scss'; import tabsGroupChildren from './themes/tabsGroupChildren.scss'; import underlineTheme from './themes/underline.scss'; +import underlineDarkTheme from './themes/underline-dark.scss'; import verticalTheme from './themes/vertical.scss'; import pillsTheme from './themes/pills.scss'; @@ -31,6 +33,7 @@ export const TAB_THEMES = { 'Tabs Group': tabsGroup, 'Tabs Group Children': tabsGroupChildren, Underline: underlineTheme, + 'Underline dark': underlineDarkTheme, Vertical: verticalTheme, Pills: pillsTheme, }; @@ -38,7 +41,38 @@ export const TAB_THEMES = { export default class TabsItemsLoader extends Component { constructor(props) { super(props); - this.state = { tabIndex: props.selected }; + this.state = { + tabIndex: props.selected || 0, + }; + + this.updatePageUrl.bind(this); + } + + componentDidMount() { + const q = getQuery(); + const { tabId } = this.props; + const { tabIndex } = this.state; + if (q.tabs && q.tabs[tabId] && Number(q.tabs[tabId]) !== tabIndex) { + this.setState({ tabIndex: Number(q.tabs[tabId]) }); + } else { + this.updatePageUrl(); + } + } + + componentDidUpdate() { + this.updatePageUrl(); + } + + updatePageUrl() { + const q = getQuery(); + const { tabId } = this.props; + const { tabIndex } = this.state; + updateQuery({ + tabs: { + ...q.tabs, + [tabId]: tabIndex || 0, + }, + }); } render() { @@ -48,6 +82,7 @@ export default class TabsItemsLoader extends Component { spaceName, environment, theme, + tabId, } = this.props; const { tabIndex } = this.state; @@ -127,6 +162,7 @@ export default class TabsItemsLoader extends Component { environment={environment} selected={fields.selected} theme={TAB_THEMES[fields.theme || 'Default']} + tabId={tabId} /> ); } @@ -171,4 +207,5 @@ TabsItemsLoader.propTypes = { environment: PT.string, selected: PT.number, theme: PT.shape().isRequired, + tabId: PT.string.isRequired, }; diff --git a/src/shared/components/Contentful/Tabs/index.jsx b/src/shared/components/Contentful/Tabs/index.jsx index a109794431..cda003fb7f 100644 --- a/src/shared/components/Contentful/Tabs/index.jsx +++ b/src/shared/components/Contentful/Tabs/index.jsx @@ -34,6 +34,7 @@ function ContentfulTabs(props) { environment={environment} selected={fields.selected} theme={TAB_THEMES[fields.theme || 'Default']} + tabId={fields.urlQueryName || id} /> ); }} diff --git a/src/shared/components/Contentful/Tabs/themes/underline-dark.scss b/src/shared/components/Contentful/Tabs/themes/underline-dark.scss new file mode 100644 index 0000000000..65ec122112 --- /dev/null +++ b/src/shared/components/Contentful/Tabs/themes/underline-dark.scss @@ -0,0 +1,101 @@ +@import "~styles/mixins"; + +$container-background-gray: #ebebeb; +$text-color-black: #262628; +$text-color-gray: #888894; +$text-color-pannel: #4a4a4a; + +.container { + @include roboto-regular; + + max-width: $screen-md; + margin: auto; +} + +.tablist { + @include roboto-regular; + + display: -webkit-flex; /* Safari */ + display: flex; + -webkit-flex-direction: row; /* Safari */ + flex-direction: row; + -webkit-justify-content: center; /* Safari */ + justify-content: center; + list-style-type: none; + border-bottom: 5px solid #013b4d; + + @include xs-to-sm { + overflow-x: auto; + white-space: nowrap; + border: none; + margin-left: 16px; + } + + @media (max-width: 425px) { + justify-content: space-between; + } +} + +.tab { + text-align: center; + margin: 0 25px; + color: #fff; + font-size: 20px; + font-weight: 400; + line-height: 25px; + padding-bottom: 15px; + cursor: pointer; + margin-bottom: -5px; + position: relative; + + @include xs-to-sm { + margin: 0; + padding-right: 36px; + border-bottom: 5px solid #013b4d; + } + + &:hover, + &.selected { + &::after { + content: ''; + border-radius: 1000vw; + background: #43d7b0; + height: 5px; + width: 100%; + position: absolute; + bottom: 0; + left: 0; + + @include xs-to-sm { + width: calc(100% - 36px); + bottom: -5px; + } + } + } + + p { + small { + color: #888894; + font-size: 13px; + font-weight: 400; + line-height: 25px; + text-align: left; + } + + strong { + font-weight: bold; + } + } +} + +.tabpannel { + display: none; +} + +.selectedTabPanel { + display: block; + + @include xs-to-sm { + padding: 0 15px; + } +} diff --git a/src/shared/components/Contentful/Tabs/themes/underline.scss b/src/shared/components/Contentful/Tabs/themes/underline.scss index 9c014b07d6..5dc5689b77 100644 --- a/src/shared/components/Contentful/Tabs/themes/underline.scss +++ b/src/shared/components/Contentful/Tabs/themes/underline.scss @@ -25,31 +25,54 @@ $text-color-pannel: #4a4a4a; border-bottom: 5px solid #e9e9e9; @include xs-to-sm { - -webkit-flex-direction: column; /* Safari */ - flex-direction: column; - align-items: center; + overflow-x: auto; + white-space: nowrap; + border: none; + margin-left: 16px; + } + + @media (max-width: 425px) { + justify-content: space-between; } } .tab { text-align: center; - margin: 0 20px; - color: $text-color-black; + margin: 0 25px; + color: #2a2a2a; font-size: 20px; font-weight: 400; line-height: 25px; padding-bottom: 15px; cursor: pointer; margin-bottom: -5px; + position: relative; @include xs-to-sm { - margin: 2px; + margin: 0; + padding-right: 36px; + border-bottom: 5px solid #e9e9e9; } &:hover, &.selected { - border-bottom: 5px solid #43d7b0; - border-radius: 3px; + &::after { + content: ''; + border-radius: 1000vw; + background: #43d7b0; + height: 5px; + width: 100%; + position: absolute; + bottom: 0; + left: 0; + border-left: 1px solid #fff; + border-right: 1px solid #fff; + + @include xs-to-sm { + width: calc(100% - 36px); + bottom: -5px; + } + } } p { @@ -73,4 +96,8 @@ $text-color-pannel: #4a4a4a; .selectedTabPanel { display: block; + + @include xs-to-sm { + padding: 0 15px; + } } diff --git a/src/shared/components/Contentful/TracksTree/TracksTree.jsx b/src/shared/components/Contentful/TracksTree/TracksTree.jsx index 28039955bf..3e780482d2 100644 --- a/src/shared/components/Contentful/TracksTree/TracksTree.jsx +++ b/src/shared/components/Contentful/TracksTree/TracksTree.jsx @@ -6,7 +6,7 @@ import _ from 'lodash'; import PT from 'prop-types'; import React, { Component } from 'react'; import { themr } from 'react-css-super-themr'; -import { config } from 'topcoder-react-utils'; +import { config, Link } from 'topcoder-react-utils'; import IconArrowUpBig from 'assets/images/tc-edu/icon-arrow-up-big.svg'; import defaultTheme from './themes/default.scss'; @@ -57,7 +57,7 @@ export class TracksTreeInner extends Component { const selectedTrack = _.find(list, { id: expandedTrack }); return (
- THRIVE + THRIVE {!isShowFullList && selectedTrack && ( @@ -86,8 +88,8 @@ class ChallengeHistoryModal extends Component { @@ -112,19 +114,19 @@ class ChallengeHistoryModal extends Component { { challengesOrdered.map(challenge => ( - + - - {challenge.challenge_name || challenge.challenge_id} + + {challenge['challenge.challenge_name'] || challenge['tco_leaderboard.challenge_id'] || challenge.challenge_id} { !isCopilot ? ( - {challenge.place} + {challenge['tco_leaderboard.placement'] || challenge.place} ) : null } - {challenge.points} + {challenge['tco_leaderboard.tco_points'] || challenge.points} )) diff --git a/src/shared/components/Leaderboard/LeaderboardTable/index.jsx b/src/shared/components/Leaderboard/LeaderboardTable/index.jsx index 2f969a9ce4..33c49b62e9 100644 --- a/src/shared/components/Leaderboard/LeaderboardTable/index.jsx +++ b/src/shared/components/Leaderboard/LeaderboardTable/index.jsx @@ -28,6 +28,8 @@ import PT from 'prop-types'; import { Avatar } from 'topcoder-react-ui-kit'; import { config } from 'topcoder-react-utils'; import _ from 'lodash'; +import DefaultAvatar from 'assets/images/default-avatar-photo.svg'; + import avatarStyles from '../avatarStyles.scss'; import defaultStyles from './themes/styles.scss'; // eslint-disable-line @@ -58,7 +60,7 @@ export default function LeaderboardTable(props) { const stylesName = THEME[themeName]; const renderTableRows = comps => ( comps.map((competitor) => { - let photoUrl = competitor.avatar; + let photoUrl = competitor['member_profile_basic.photo_url'] || competitor.avatar; if (photoUrl) { photoUrl = `${config.CDN.PUBLIC}/avatar/${ encodeURIComponent(photoUrl)}?size=40`; @@ -68,12 +70,16 @@ export default function LeaderboardTable(props) { {competitor.rank} - + { + photoUrl ? ( + + ) : + } @@ -83,18 +89,18 @@ export default function LeaderboardTable(props) { styleName={`${stylesName}.handle-link`} onClick={() => onUsernameClick(competitor)} > - {competitor.handle} + {competitor['member_profile_basic.handle'] || competitor.handle}
) : ( - - {competitor.handle} + + {competitor['member_profile_basic.handle'] || competitor.handle} ) }
{competitor.fulfillment && ({competitor.fulfillment} fulfillment)} - {competitor.points} points - {competitor.challengecount} challenges + {competitor['tco_leaderboard.tco_points'] || competitor.points} points + {competitor['tco_leaderboard.challenge_count'] || competitor.challengecount} challenges
{ @@ -102,8 +108,8 @@ export default function LeaderboardTable(props) { {competitor.fulfillment} ) : null } - {competitor.challengecount} - {formatPoints(competitor.points)} + {competitor['tco_leaderboard.challenge_count'] || competitor.challengecount} + {formatPoints(competitor['tco_leaderboard.tco_points'] || competitor.points)} { isTopGear ? ( {competitor.wins} diff --git a/src/shared/components/Leaderboard/LeaderboardTable/themes/tco20.scss b/src/shared/components/Leaderboard/LeaderboardTable/themes/tco20.scss index fe4938c6b1..a77a4a8e4a 100644 --- a/src/shared/components/Leaderboard/LeaderboardTable/themes/tco20.scss +++ b/src/shared/components/Leaderboard/LeaderboardTable/themes/tco20.scss @@ -118,6 +118,11 @@ $table-bg-hover: #f5f5f5; height: 40px; width: 40px; overflow: hidden; + + svg { + border: 3px solid rgba(0, 0, 0, 0.05); + border-radius: 50%; + } } .col-handle { diff --git a/src/shared/components/Leaderboard/PodiumSpot/index.jsx b/src/shared/components/Leaderboard/PodiumSpot/index.jsx index 8a357fb00b..72fc4869d8 100644 --- a/src/shared/components/Leaderboard/PodiumSpot/index.jsx +++ b/src/shared/components/Leaderboard/PodiumSpot/index.jsx @@ -29,6 +29,7 @@ import PT from 'prop-types'; import { Avatar } from 'topcoder-react-ui-kit'; import { config } from 'topcoder-react-utils'; import _ from 'lodash'; +import DefaultAvatar from 'assets/images/default-avatar-photo.svg'; import avatarStyles from '../avatarStyles.scss'; import defaultStyles from './themes/styles.scss'; // eslint-disable-line @@ -99,7 +100,7 @@ export default function PodiumSpot(props) { } = props; const stylesName = THEME[themeName]; - let photoUrl = competitor.avatar; + let photoUrl = competitor['member_profile_basic.photo_url'] || competitor.avatar; if (photoUrl) { photoUrl = `${config.CDN.PUBLIC}/avatar/${ encodeURIComponent(photoUrl)}?size=160`; @@ -110,12 +111,16 @@ export default function PodiumSpot(props) { return (
- + { + photoUrl ? ( + + ) : + }
{DISPLAY_RANKING[competitor.rank]}
{ @@ -127,15 +132,15 @@ export default function PodiumSpot(props) { styleName={`${stylesName}.handle-link`} onClick={() => onUsernameClick(competitor)} > - {competitor.handle} + {competitor['member_profile_basic.handle'] || competitor.handle}
) : ( - {competitor.handle} + {competitor['member_profile_basic.handle'] || competitor.handle} ) } @@ -152,15 +157,15 @@ export default function PodiumSpot(props) { styleName={`${stylesName}.handle-link`} onClick={() => onUsernameClick(competitor)} > - {competitor.handle} + {competitor['member_profile_basic.handle'] || competitor.handle}
) : ( - {competitor.handle} + {competitor['member_profile_basic.handle'] || competitor.handle} ) } @@ -176,11 +181,11 @@ export default function PodiumSpot(props) { ) : null }
- {competitor.challengecount} + {competitor['tco_leaderboard.challenge_count'] || competitor.challengecount} challenges
- {formatPoints(competitor.points)} + {formatPoints(competitor['tco_leaderboard.tco_points'] || competitor.points)} points
{ diff --git a/src/shared/containers/EDU/partials/TrackCards.jsx b/src/shared/containers/EDU/partials/TrackCards.jsx index 7bf1805436..50f52334e0 100644 --- a/src/shared/containers/EDU/partials/TrackCards.jsx +++ b/src/shared/containers/EDU/partials/TrackCards.jsx @@ -13,7 +13,7 @@ export default function TrackCards(props) { content_type: 'article', 'fields.trackCategory': track, limit: 3, - order: 'sys.createdAt', + order: '-sys.createdAt', }} spaceName="EDU" render={(data) => { diff --git a/src/shared/containers/EDU/partials/TrackInfoInner.jsx b/src/shared/containers/EDU/partials/TrackInfoInner.jsx index 87ec321196..d576078ab1 100644 --- a/src/shared/containers/EDU/partials/TrackInfoInner.jsx +++ b/src/shared/containers/EDU/partials/TrackInfoInner.jsx @@ -1,22 +1,22 @@ import _ from 'lodash'; import React from 'react'; import PT from 'prop-types'; -import { config } from 'topcoder-react-utils'; +import { config, Link } from 'topcoder-react-utils'; import qs from 'qs'; export default function TrackInfoInner(props) { const { track, taxonomy, theme } = props; return (
- + {track} - + { taxonomy[track] ? (
{ _.map( - _.sortBy(taxonomy[track], ['name']), tax => {tax.name}, + _.sortBy(taxonomy[track], ['name']), tax => {tax.name}, ) }
diff --git a/src/shared/utils/contentful.js b/src/shared/utils/contentful.js index 3c253aa36b..a7b4880310 100644 --- a/src/shared/utils/contentful.js +++ b/src/shared/utils/contentful.js @@ -9,7 +9,14 @@ import { removeTrailingSlash } from 'utils/url'; * @return {Object} */ export function fixStyle(style) { - return style ? _.mapKeys(style, (value, key) => _.camelCase(key)) : undefined; + const props = _.omitBy(style, !_.isObject); + const mediaQueries = _.pickBy( + style, + (propVal, mQuery) => _.isObject(propVal) + && isomorphy.isClientSide() && window.matchMedia(mQuery).matches, + ); + const merged = _.merge(props, ..._.values(mediaQueries)); + return merged ? _.mapKeys(merged, (value, key) => _.camelCase(key)) : undefined; } // Concatenates a base and segment and handles optional trailing slashes diff --git a/src/shared/utils/url.js b/src/shared/utils/url.js index 73154aa74a..dc2a90d0ac 100644 --- a/src/shared/utils/url.js +++ b/src/shared/utils/url.js @@ -8,6 +8,22 @@ import _ from 'lodash'; import qs from 'qs'; import { isomorphy } from 'topcoder-react-utils'; +/** + * Get current URL hash parameters as object + */ +export function getHash() { + if (isomorphy.isServerSide()) return null; + return qs.parse(window.location.hash.slice(1)); +} + +/** + * Get current URL query parameters as object + */ +export function getQuery() { + if (isomorphy.isServerSide()) return {}; + return qs.parse(window.location.search.slice(1)); +} + /** * If executed client-side (determined in this case by the presence of global * window object), this function updates query section of URL; otherwise does @@ -22,6 +38,7 @@ export function updateQuery(update) { if (isomorphy.isServerSide()) return; let query = qs.parse(window.location.search.slice(1)); + const { hash } = window.location; /* _.merge won't work here, because it just ignores the fields explicitely * set as undefined in the objects to be merged, rather than deleting such @@ -31,6 +48,9 @@ export function updateQuery(update) { else query[key] = value; }); query = `?${qs.stringify(query, { encodeValuesOnly: true })}`; + if (hash) { + query += hash; + } window.history.replaceState(window.history.state, '', query); }