]*href="([^"]+)"/gm.exec(HTMLsrc);
- if (match && match[1]) {
- // create the asset in Contentful
- return env.createAsset({
- fields: {
- title: { 'en-US': blogPost.title },
- file: {
- 'en-US': { fileName: blogPost.title, contentType: 'image', upload: match[1] },
- },
- },
- })
- .then(asset => asset.processForAllLocales())
- .then((asset) => {
- article.fields.featuredImage = {
- 'en-US': {
- sys: {
- type: 'Link', linkType: 'Asset', id: asset.sys.id,
- },
- },
- };
- return env.createEntry('article', article);
- });
- }
- // could not find image
- // just create the article without it...
- return env.createEntry('article', article);
- });
- });
- }
- // Article exists in Contentful space
- // do nothing...
- return 1;
- })),
- )
- .then(() => logger.log('polling blog articles for THRIVE -> DONE'));
- }
- }
- // schedule a repeat each TC_EDU_POLL_TIME
- setTimeout(
- pollArticlesForThrive,
- pollInt * 1000 + (Math.floor(Math.random() * Math.floor(5)) * 60 * 1000),
- ).unref();
-}
-
-// /* Contentful CDN service. */
-// export const cdnService = new ApiService(CDN_URL, CDN_KEY);
-
-// /* Contentful Preview service. */
-// export const previewService = new ApiService(PREVIEW_URL, PREVIEW_KEY);
-
-
let services;
function initServiceInstances() {
- const contentfulConfig = config.SECRET.CONTENTFUL;
+ const contentfulConfig = _.omit(config.SECRET.CONTENTFUL, [
+ 'DEFAULT_SPACE_NAME', 'DEFAULT_ENVIRONMENT', 'MANAGEMENT_TOKEN',
+ ]);
services = {};
_.map(contentfulConfig, (spaceConfig, spaceName) => {
services[spaceName] = {};
@@ -338,8 +154,10 @@ function initServiceInstances() {
const cdnBaseUrl = `${CDN_URL}/${spaceId}/environments/${environment}`;
const svcs = {};
- svcs.previewService = new ApiService(previewBaseUrl.toString(), env.PREVIEW_API_KEY);
- svcs.cdnService = new ApiService(cdnBaseUrl.toString(), env.CDN_API_KEY);
+ svcs.previewService = new ApiService(
+ previewBaseUrl.toString(), env.PREVIEW_API_KEY, spaceId, true,
+ );
+ svcs.cdnService = new ApiService(cdnBaseUrl.toString(), env.CDN_API_KEY, spaceId);
services[spaceName][environment] = svcs;
}
});
@@ -378,302 +196,3 @@ export function getService(spaceName, environment, preview) {
const service = services[name][env];
return preview ? service.previewService : service.cdnService;
}
-
-/**
- * Generates the last version for content index, and other similar data that
- * have to be refreshed regularly to keep us in sync with content edits in CMS.
- * @return {Number}
- */
-/*
-function getLastVersion() {
- const now = Date.now();
- return now - (now % INDEX_MAXAGE);
-}
-*/
-
-/**
- * Gets the index of assets and entries via Community App CDN.
- * @param {Number} version Optional. The version of index to fetch. Defaults to
- * the latest index version.
- * @return {Promise}
- */
-/*
-async function getIndexViaCdn(version = getLastVersion()) {
- const res = await fetch(`${TC_CDN_URL}/index?version=${version}`);
- if (!res.ok) {
- const MSG = 'Failed to get the index';
- logger.error(MSG, res);
- throw new Error(MSG);
- }
- return res.json();
-}
-*/
-
-/**
- * Gets the next sync URL via CDN.
- * @param {Number} version Optional. The version of index to fetch. Defaults to
- * the latest version.
- * @return {Promise}
- */
-/*
-async function getNextSyncUrlViaCdn(version = getLastVersion()) {
- const res =
- await fetch(`${TC_CDN_URL}/next-sync-url?version=${version}`);
- if (!res.ok) {
- const MSG = 'Failed to get the next sync URL';
- logger.error(MSG, res.statusText);
- throw new Error(MSG);
- }
- return res.text();
-}
-*/
-
-/* THE INDEX OF CMS ASSETS AND ENTRIES.
- *
- * A tricky logic is involved to keep it all working properly, beware to modify
- * and in all cases prefer to use exported functions that provide access to the
- * index and take care about its correct updating. */
-
-/* The barrier for syncronization of parallel calls to async functions exported
- * by this module. If not null, then it is a promise that signals that the index
- * update is in progress; in this case any function that needs to access the
- * index should wait until the promise is resolved. */
-// let barrier = null;
-
-/* Holds the next sync URL provided by the CMS. */
-// let nextSyncUrl;
-
-/* The public index of CMS assets and entries. It is the map between CMS IDs of
- * these assets/entries and the timestamps of their last updates. Note that this
- * index is accessible by the frontend via CDN, thus anybody can access it and
- * thus all assets and entries mentioned in this index. That's why announcements
- * with future startDate are not included into this index. */
-// let publicIndex;
-
-/**
- * Adds a new asset to the index, or updates the existing one.
- * @param {Object} asset
- */
-/*
-function indexAsset(asset) {
- const { id, createdAt, updatedAt } = asset.sys;
- publicIndex.assets[id] = moment(updatedAt || createdAt).valueOf();
-}
-*/
-
-/**
- * Adds a new entry to the index, or updates the existing one.
- * @param {Object} entry
- */
-/*
-function indexEntry(entry) {
- let isPublic = true;
- const { id, createdAt, updatedAt } = entry.sys;
- const timestamp = moment(updatedAt || createdAt).valueOf();
-
- const type = entry.sys.contentType.sys.id;
- switch (type) {
- /* We use an additional index of dashboard announcement to be able to find
- * out which announcement should be show at any moment, without a call to
- * CMS. We also do not include future announcements into the public index
- * to avoid any exposure to general public before the time. */
-/*
- case 'dashboardAnnouncement': {
- const now = Date.now();
- const endDate = moment(entry.fields.endDate['en-US']).valueOf();
- const startDate = moment(entry.fields.startDate['en-US']).valueOf();
- isPublic = now > startDate;
- if (now < endDate) {
- currentDashboardAnnouncementsMap[id] = {
- id,
- endDate,
- startDate,
- timestamp,
- };
- }
- break;
- }
- default:
- }
-
- if (isPublic) publicIndex.entries[id] = timestamp;
-}
-*/
-
-/**
- * Adds a new asset or entry to the index, or updates / removes the existing
- * one.
- * @param {Object} item
- */
-/*
-function indexItem(item) {
- const { id, type } = item.sys;
- switch (type) {
- case 'Asset': indexAsset(item); break;
- case 'DeletedAsset': delete publicIndex.assets[id]; break;
- case 'DeletedEntry':
- delete currentDashboardAnnouncementsMap[id];
- delete publicIndex.entries[id];
- break;
- case 'Entry': indexEntry(item); break;
- default: throw new Error('Invariant violation');
- }
-}
-*/
-
-/**
- * Updates the current announcement ID.
- */
-/*
-function updateCurrentDashboardAnnouncementId() {
- const list = [];
- const now = Date.now();
- Object.values(currentDashboardAnnouncementsMap).forEach((item) => {
- if (item.endDate < now) delete currentDashboardAnnouncementsMap[item.id];
- else if (item.startDate < now) list.push(item);
- });
- if (list.length) {
- list.sort((a, b) => b.startDate - a.startDate);
- currentDashboardAnnouncementId = list[0].id;
- } else currentDashboardAnnouncementId = '';
- if (currentDashboardAnnouncementId
- && (!publicIndex.entries[currentDashboardAnnouncementId])) {
- publicIndex.entries[currentDashboardAnnouncementId] = list[0].timestamp;
- }
-}
-*/
-
-/**
- * Updates the index.
- * @return {Promise}
- */
-/*
-async function updateIndex() {
- let nextPageUrl = nextSyncUrl;
- while (nextPageUrl) {
- /* Disabled, as we really need to keep these iterations sequential, thus
- * await inside the loop is not an error. */
-/* eslint-disable no-await-in-loop */
-/*
- let d = await fetch(nextPageUrl, {
- headers: { Authorization: `Bearer ${CDN_KEY}` },
- });
- if (!d.ok) {
- const MSG = 'Failed to update the index';
- logger.error(MSG, d.statusText);
- throw new Error(MSG);
- }
- d = await d.json();
- /* eslint-anable no-await-in-loop */
-/*
- d.items.forEach(indexItem);
- ({ nextPageUrl, nextSyncUrl } = d);
- }
- publicIndex.timestamp = Date.now();
- updateCurrentDashboardAnnouncementId();
-}
-*/
-
-/**
- * Inits the index with data from CMS.
- * @return {Promise}
- */
-/*
-async function initIndex() {
- /* Gets necessary data from CMS. */
-/*
- let d = await fetch(`${CDN_URL}/sync?initial=true`, {
- headers: { Authorization: `Bearer ${CDN_KEY}` },
- });
- if (!d.ok) {
- const MSG = 'Failed to initialize the index';
- logger.error(MSG, d.statusText);
- throw new Error(MSG);
- }
- d = await d.json();
-
- /* Generates the index. */
-/*
- publicIndex = {
- assets: {},
- entries: {},
- };
- currentDashboardAnnouncementsMap = {};
- d.items.forEach(indexItem);
- publicIndex.timestamp = Date.now();
- updateCurrentDashboardAnnouncementId();
-
- /* In case the initial update is too large to fit into a single response.
- * TODO: This updateIndex(..) function can be combined with initIndex(..)
- * into a single function. The URL query is the only real difference between
- * them. */
-/*
- if (d.nextPageUrl) {
- nextSyncUrl = d.nextPageUrl;
- await updateIndex();
- } else ({ nextSyncUrl } = d);
-}
-*/
-
-/**
- * Returns the index of CMS assets and content, along with the timestamps of
- * their last updates. This function also takes care about initialization and
- * automatic updates of the index, as necessary.
- * @return {Promise}
- */
-/*
-export async function getIndex() {
- while (barrier) await barrier;
- if (!publicIndex) barrier = initIndex();
- else if (Date.now() - publicIndex.timestamp > INDEX_MAXAGE) {
- barrier = updateIndex();
- }
- if (barrier) {
- await barrier;
- barrier = null;
-
- /* These two calls are necessary to cache the updated index by CDN. */
-/*
- getIndexViaCdn();
- getCurrentDashboardAnnouncementsIndexViaCdn();
- getNextSyncUrlViaCdn();
- }
-
- return publicIndex;
-}
-*/
-
-/**
- * Returns the next sync URL.
- * @return {Promise}
- */
-/*
-export async function getNextSyncUrl() {
- while (barrier) await barrier;
- if (!publicIndex || Date.now() - publicIndex.timestamp > INDEX_MAXAGE) {
- await getIndex();
- }
- return nextSyncUrl;
-}
-*/
-
-/* Module initialization.
- * This code tries to pull the current index from CDN, where it is supposed to
- * be cached, to keep it persistent across re-deployments of the app, and also
- * to prevent unnecessary calls to Contentful APIs from a locally deployed
- * server. In case of failure, it initializes the new index using getIndex()
- * function directly. */
-/*
-let version = Date.now() - INDEX_MAXAGE;
-version -= version % INDEX_MAXAGE;
-Promise.all([
- getIndexViaCdn(version),
- getCurrentDashboardAnnouncementsIndexViaCdn(version),
- getNextSyncUrl(version),
-]).then(([index, dashIndex, next]) => {
- publicIndex = index;
- currentDashboardAnnouncementsMap = dashIndex;
- nextSyncUrl = next;
- updateCurrentDashboardAnnouncementId();
-}).catch(() => getIndex());
-*/
diff --git a/src/shared/components/Contentful/Article/Article.jsx b/src/shared/components/Contentful/Article/Article.jsx
index 2beb028f37..c0ef0eae71 100644
--- a/src/shared/components/Contentful/Article/Article.jsx
+++ b/src/shared/components/Contentful/Article/Article.jsx
@@ -8,6 +8,8 @@ import PT from 'prop-types';
import { fixStyle } from 'utils/contentful';
import { getService } from 'services/contentful';
import MarkdownRenderer from 'components/MarkdownRenderer';
+import ReactDOMServer from 'react-dom/server';
+import markdown from 'utils/markdown';
import ContentfulLoader from 'containers/ContentfulLoader';
import LoadingIndicator from 'components/LoadingIndicator';
import YouTubeVideo from 'components/YouTubeVideo';
@@ -21,6 +23,8 @@ import UserDefault from 'assets/images/ico-user-default.svg';
import ReadMoreArrow from 'assets/images/read-more-arrow.svg';
import qs from 'qs';
+const htmlToText = require('html-to-text');
+
// character length for the content preview
const CONTENT_PREVIEW_LENGTH = 110;
// Votes local storage key
@@ -254,7 +258,17 @@ export default class Article extends React.Component {
{
- `${subData.entries.items[rec.sys.id].fields.content.substring(0, CONTENT_PREVIEW_LENGTH)}..`
+ `${htmlToText.fromString(
+ ReactDOMServer.renderToString(markdown(
+ subData.entries.items[rec.sys.id].fields.content,
+ )),
+ {
+ ignoreHref: true,
+ ignoreImage: true,
+ singleNewLineParagraphs: true,
+ uppercaseHeadings: false,
+ },
+ ).substring(0, CONTENT_PREVIEW_LENGTH)}...`
}
{
diff --git a/src/shared/components/Contentful/ArticleCard/ArticleCard.jsx b/src/shared/components/Contentful/ArticleCard/ArticleCard.jsx
index e153e91d5d..59a6e794bd 100644
--- a/src/shared/components/Contentful/ArticleCard/ArticleCard.jsx
+++ b/src/shared/components/Contentful/ArticleCard/ArticleCard.jsx
@@ -12,6 +12,7 @@ import React from 'react';
import { themr } from 'react-css-super-themr';
import { config } from 'topcoder-react-utils';
import markdown from 'utils/markdown';
+import ReactDOMServer from 'react-dom/server';
// SVG assets
import ThumbUpIcon from 'assets/images/ico-thumb-up.svg';
import CommentIcon from 'assets/images/ico-comment.svg';
@@ -21,17 +22,18 @@ import PlayIcon from 'assets/images/ico-play.svg';
import ReadMoreArrow from 'assets/images/read-more-arrow.svg';
import qs from 'qs';
+const htmlToText = require('html-to-text');
+
// date/time format to use for the creation date
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;
-// character length for the content preview
-const CONTENT_PREVIEW_LENGTH = 110;
// Article large card 'breakpoint'
const ARTICLE_LARGE_BREAKPOINT = 473;
-
+// character length for the content preview
+const CONTENT_PREVIEW_LENGTH = 110;
class ArticleCard extends React.Component {
constructor(props) {
@@ -132,10 +134,16 @@ class ArticleCard extends React.Component {
: article.title;
// truncate content for 'Article large' cards
- const content = (
- (themeName === 'Article large' || themeName === 'Recommended')
- && article.content.length > CONTENT_PREVIEW_LENGTH)
- ? markdown(`${article.content.substring(0, CONTENT_PREVIEW_LENGTH)}...`)
+ const content = themeName === 'Article large' || themeName === 'Recommended'
+ ? `${htmlToText.fromString(
+ ReactDOMServer.renderToString(markdown(article.content)),
+ {
+ ignoreHref: true,
+ ignoreImage: true,
+ singleNewLineParagraphs: true,
+ uppercaseHeadings: false,
+ },
+ ).substring(0, CONTENT_PREVIEW_LENGTH)}...`
: undefined;
// set the correct format to apply to the `article.creationDate`
diff --git a/src/shared/components/ProfilePage/Header/index.jsx b/src/shared/components/ProfilePage/Header/index.jsx
index c070adfa76..51e716b1cd 100644
--- a/src/shared/components/ProfilePage/Header/index.jsx
+++ b/src/shared/components/ProfilePage/Header/index.jsx
@@ -7,7 +7,7 @@ import { noop, get } from 'lodash';
import moment from 'moment';
import ReactSVG from 'react-svg';
-import { getRatingColor } from 'utils/tc';
+import { getRatingLevel } from 'utils/tc';
import { config, isomorphy } from 'topcoder-react-utils';
import CopilotIcon from 'assets/images/profile/ico-track-copilot.svg';
@@ -63,7 +63,7 @@ class ProfileHeader extends React.Component {
{ imageUrl ?
: }
-
+
{info.handle}
diff --git a/src/shared/components/ProfilePage/Header/styles.scss b/src/shared/components/ProfilePage/Header/styles.scss
index e0dfffbc16..547dd3006d 100644
--- a/src/shared/components/ProfilePage/Header/styles.scss
+++ b/src/shared/components/ProfilePage/Header/styles.scss
@@ -39,6 +39,26 @@
line-height: 34px;
@include sofia-pro-medium;
+
+ &.level-1 {
+ color: $tc-level-1;
+ }
+
+ &.level-2 {
+ color: $tc-level-2;
+ }
+
+ &.level-3 {
+ color: $tc-level-3;
+ }
+
+ &.level-4 {
+ color: $tc-level-4;
+ }
+
+ &.level-5 {
+ color: $tc-level-5;
+ }
}
h3.tenure {
diff --git a/src/shared/components/ProfilePage/index.jsx b/src/shared/components/ProfilePage/index.jsx
index a87cfc8ac2..92195ee203 100644
--- a/src/shared/components/ProfilePage/index.jsx
+++ b/src/shared/components/ProfilePage/index.jsx
@@ -119,7 +119,6 @@ class ProfilePage extends React.Component {
copilot,
externalAccounts,
externalLinks,
- info,
skills: propSkills,
stats,
lookupData,
@@ -131,6 +130,12 @@ class ProfilePage extends React.Component {
skillsExpanded,
} = this.state;
+ let { info } = this.props;
+
+ if (_.isNull(_.get(info, 'maxRating.rating', 0)) && !_.isEmpty(stats)) {
+ info = _.assign(info, { maxRating: stats[0].maxRating });
+ }
+
// get country
let country = '';
if (_.has(lookupData, 'countries') && lookupData.countries.length > 0) {
diff --git a/src/shared/components/SubmissionPage/Uploading/styles.scss b/src/shared/components/SubmissionPage/Uploading/styles.scss
index 7dbf48d28d..d39f367a05 100644
--- a/src/shared/components/SubmissionPage/Uploading/styles.scss
+++ b/src/shared/components/SubmissionPage/Uploading/styles.scss
@@ -4,15 +4,14 @@
@include roboto-regular;
align-items: center;
- position: absolute;
width: 100%;
- height: calc(100vh);
background-color: white;
- top: -54px;
left: 0;
z-index: 100000;
display: flex;
justify-content: center;
+ position: relative;
+ padding: 30px;
}
.link,
diff --git a/src/shared/components/SubmissionPage/styles.scss b/src/shared/components/SubmissionPage/styles.scss
index 4fabe9d902..7837315c20 100644
--- a/src/shared/components/SubmissionPage/styles.scss
+++ b/src/shared/components/SubmissionPage/styles.scss
@@ -4,8 +4,8 @@
display: flex;
flex: 1;
justify-content: center;
- padding: 30px 0 40px;
- background: #f6f6f6;
+ padding: 80px 0 40px;
+ background: #fff;
@include md-to-lg {
padding-bottom: 0;
diff --git a/src/shared/components/TopcoderFooter/index.jsx b/src/shared/components/TopcoderFooter/index.jsx
index 70acbf4905..6f97a5c674 100644
--- a/src/shared/components/TopcoderFooter/index.jsx
+++ b/src/shared/components/TopcoderFooter/index.jsx
@@ -89,6 +89,7 @@ export default function TopcoderFooter() {
Contact Us
Join Community
About Community
+ Changelog
Talk to Sales
diff --git a/src/shared/components/challenge-detail/Specification/index.jsx b/src/shared/components/challenge-detail/Specification/index.jsx
index 266418090c..3f425545c8 100644
--- a/src/shared/components/challenge-detail/Specification/index.jsx
+++ b/src/shared/components/challenge-detail/Specification/index.jsx
@@ -475,11 +475,11 @@ export default function ChallengeDetailsView(props) {
please refer to
- https://help.topcoder.com/hc/en-us/articles/217482038-Payment-Policies-and-Instructions
+ https://www.topcoder.com/thrive/articles/Payment%20Policies%20and%20Instructions
)
diff --git a/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx b/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx
index c32da2cc94..521441e08d 100644
--- a/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx
+++ b/src/shared/components/challenge-listing/Filters/ChallengeFilters.jsx
@@ -57,11 +57,17 @@ export default function ChallengeFilters({
setFilterState(filterObj);
};
+ const clearSearch = () => {
+ setFilterState(Filter.setText(filterState, ''));
+ setSearchText('');
+ };
+
return (
setFilterState(Filter.setText(filterState, text))}
+ onClearSearch={() => clearSearch()}
label={isReviewOpportunitiesBucket ? 'Search Review Opportunities:' : 'Search Challenges:'}
placeholder={isReviewOpportunitiesBucket ? 'Search Review Opportunities' : 'Type the challenge name here'}
query={searchText}
diff --git a/src/shared/components/challenge-listing/Filters/ChallengeSearchBar/index.jsx b/src/shared/components/challenge-listing/Filters/ChallengeSearchBar/index.jsx
index 13f9756f50..986e987144 100644
--- a/src/shared/components/challenge-listing/Filters/ChallengeSearchBar/index.jsx
+++ b/src/shared/components/challenge-listing/Filters/ChallengeSearchBar/index.jsx
@@ -19,6 +19,7 @@ import ZoomIcon from './ui-zoom.svg';
export default function ChallengeSearchBar({
onSearch,
+ onClearSearch,
placeholder,
query,
setQuery,
@@ -35,6 +36,13 @@ export default function ChallengeSearchBar({
type="text"
value={query}
/>
+ onClearSearch()}
+ onKeyPress={() => onClearSearch()}
+ >
+ ⨯
+
onSearch(query.trim())}
@@ -56,6 +64,7 @@ ChallengeSearchBar.defaultProps = {
ChallengeSearchBar.propTypes = {
onSearch: PT.func.isRequired,
+ onClearSearch: PT.func.isRequired,
label: PT.string,
placeholder: PT.string,
query: PT.string.isRequired,
diff --git a/src/shared/components/challenge-listing/Filters/ChallengeSearchBar/style.scss b/src/shared/components/challenge-listing/Filters/ChallengeSearchBar/style.scss
index 0c295c25cf..08cfe39428 100644
--- a/src/shared/components/challenge-listing/Filters/ChallengeSearchBar/style.scss
+++ b/src/shared/components/challenge-listing/Filters/ChallengeSearchBar/style.scss
@@ -4,6 +4,7 @@ $challenge-space-15: $base-unit * 3;
$challenge-space-20: $base-unit * 4;
$challenge-space-30: $base-unit * 6;
$challenge-space-40: $base-unit * 8;
+$challenge-space-60: $base-unit * 12;
$challenge-radius-1: $corner-radius / 2;
$challenge-radius-4: $corner-radius * 2;
@@ -43,6 +44,7 @@ $search-input-width: '100% - 56px';
margin-bottom: 0;
margin-left: 10px;
vertical-align: middle;
+ padding-right: 28px;
@include placeholder {
@include roboto-light;
@@ -77,4 +79,24 @@ $search-input-width: '100% - 56px';
width: 16px;
}
}
+
+ .ClearButton {
+ display: none;
+ cursor: pointer;
+ position: absolute;
+ right: $challenge-space-60 - 3;
+ padding: $base-unit;
+ border-radius: 0 $challenge-radius-4 $challenge-radius-4 0;
+ vertical-align: middle;
+ height: 38px;
+ font-size: 25px;
+
+ &.active {
+ display: inline-block;
+ }
+
+ &:hover {
+ color: $tc-red-110;
+ }
+ }
}
diff --git a/src/shared/containers/EDU/styles/tabs.scss b/src/shared/containers/EDU/styles/tabs.scss
index 2345eae506..eee81a60b5 100644
--- a/src/shared/containers/EDU/styles/tabs.scss
+++ b/src/shared/containers/EDU/styles/tabs.scss
@@ -41,6 +41,7 @@
border-radius: 9px;
margin-left: 9px;
padding: 0 7px;
+ padding-top: 2px;
}
&.selected {