diff --git a/.circleci/config.yml b/.circleci/config.yml index b789da1770..acfef22d2f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -175,14 +175,15 @@ workflows: branches: only: - develop - - hot-fix-profile-issue # This is alternate dev env for parallel testing - "build-test": context : org-global filters: branches: only: + - develop - feature-contentful + - hot-fix-content-at-bottom-of-challenge-details # This is beta env for production soft releases - "build-prod-beta": context : org-global @@ -190,7 +191,6 @@ workflows: branches: only: - develop - - hotfix-legacy-mm # Production builds are exectuted # when PR is merged to the master # Don't change anything in this configuration diff --git a/__tests__/shared/components/Contentful/SearchBar/SearchBar.jsx b/__tests__/shared/components/Contentful/SearchBar/SearchBar.jsx deleted file mode 100644 index f4c032931f..0000000000 --- a/__tests__/shared/components/Contentful/SearchBar/SearchBar.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { SearchBarInner } from 'components/Contentful/SearchBar/SearchBar'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - selected: '', - options: [], - theme: {}, - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/SearchBar/__snapshots__/SearchBar.jsx.snap b/__tests__/shared/components/Contentful/SearchBar/__snapshots__/SearchBar.jsx.snap deleted file mode 100644 index 1772d9ce45..0000000000 --- a/__tests__/shared/components/Contentful/SearchBar/__snapshots__/SearchBar.jsx.snap +++ /dev/null @@ -1,53 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
-
- - -
- -
-
-
-`; diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterAuthor/FilterAuthor.jsx b/__tests__/shared/components/Contentful/SearchPageFilter/FilterAuthor/FilterAuthor.jsx deleted file mode 100644 index 5c40cfd351..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterAuthor/FilterAuthor.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { FilterAuthorInner } from 'components/Contentful/SearchPageFilter/FilterAuthor'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - selected: 'All authors', - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterAuthor/__snapshots__/FilterAuthor.jsx.snap b/__tests__/shared/components/Contentful/SearchPageFilter/FilterAuthor/__snapshots__/FilterAuthor.jsx.snap deleted file mode 100644 index e8c4b0a310..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterAuthor/__snapshots__/FilterAuthor.jsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
- - Author - - -
-`; diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterDate/FilterDate.jsx b/__tests__/shared/components/Contentful/SearchPageFilter/FilterDate/FilterDate.jsx deleted file mode 100644 index 37c02c2333..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterDate/FilterDate.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { FilterDateInner } from 'components/Contentful/SearchPageFilter/FilterDate'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterDate/__snapshots__/FilterDate.jsx.snap b/__tests__/shared/components/Contentful/SearchPageFilter/FilterDate/__snapshots__/FilterDate.jsx.snap deleted file mode 100644 index caf83b4709..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterDate/__snapshots__/FilterDate.jsx.snap +++ /dev/null @@ -1,38 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
- - From - - - - - - - - -
-`; diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterRadio/FilterRadio.jsx b/__tests__/shared/components/Contentful/SearchPageFilter/FilterRadio/FilterRadio.jsx deleted file mode 100644 index 9985ed1b5d..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterRadio/FilterRadio.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { FilterRadioInner } from 'components/Contentful/SearchPageFilter/FilterRadio'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterRadio/__snapshots__/FilterRadio.jsx.snap b/__tests__/shared/components/Contentful/SearchPageFilter/FilterRadio/__snapshots__/FilterRadio.jsx.snap deleted file mode 100644 index 2f9f770d7b..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterRadio/__snapshots__/FilterRadio.jsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
-`; diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterSelection/FilterSelection.jsx b/__tests__/shared/components/Contentful/SearchPageFilter/FilterSelection/FilterSelection.jsx deleted file mode 100644 index 9fa14b0fc4..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterSelection/FilterSelection.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { FilterSelectionInner } from 'components/Contentful/SearchPageFilter/FilterSelection'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterSelection/__snapshots__/FilterSelection.jsx.snap b/__tests__/shared/components/Contentful/SearchPageFilter/FilterSelection/__snapshots__/FilterSelection.jsx.snap deleted file mode 100644 index 2f9f770d7b..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterSelection/__snapshots__/FilterSelection.jsx.snap +++ /dev/null @@ -1,7 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
-`; diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterTags/FilterTags.jsx b/__tests__/shared/components/Contentful/SearchPageFilter/FilterTags/FilterTags.jsx deleted file mode 100644 index a6984ff231..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterTags/FilterTags.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { FilterTagsInner } from 'components/Contentful/SearchPageFilter/FilterTags'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/FilterTags/__snapshots__/FilterTags.jsx.snap b/__tests__/shared/components/Contentful/SearchPageFilter/FilterTags/__snapshots__/FilterTags.jsx.snap deleted file mode 100644 index 511ea5807c..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/FilterTags/__snapshots__/FilterTags.jsx.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
- -
-
-`; diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/SearchPageFilter.jsx b/__tests__/shared/components/Contentful/SearchPageFilter/SearchPageFilter.jsx deleted file mode 100644 index 54b8282bac..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/SearchPageFilter.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { SearchPageFilterInner } from 'components/Contentful/SearchPageFilter/SearchPageFilter'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - startDate: '2019-08-01T07:21:20.249Z', - endDate: '2019-09-01T07:21:20.249Z', - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/SearchPageFilter/__snapshots__/SearchPageFilter.jsx.snap b/__tests__/shared/components/Contentful/SearchPageFilter/__snapshots__/SearchPageFilter.jsx.snap deleted file mode 100644 index e72483934a..0000000000 --- a/__tests__/shared/components/Contentful/SearchPageFilter/__snapshots__/SearchPageFilter.jsx.snap +++ /dev/null @@ -1,94 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
-
- - filter - - -
-
-
- - category - -
- -
-
-
- - filter - -
- - - -
-
- -
-
-`; diff --git a/__tests__/shared/components/Contentful/TracksFilter/TracksAuthor/TracksAuthor.jsx b/__tests__/shared/components/Contentful/TracksFilter/TracksAuthor/TracksAuthor.jsx deleted file mode 100644 index 8a3183493d..0000000000 --- a/__tests__/shared/components/Contentful/TracksFilter/TracksAuthor/TracksAuthor.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { TracksAuthorInner } from 'components/Contentful/TracksFilter/TracksAuthor'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - selected: 'All authors', - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/TracksFilter/TracksAuthor/__snapshots__/TracksAuthor.jsx.snap b/__tests__/shared/components/Contentful/TracksFilter/TracksAuthor/__snapshots__/TracksAuthor.jsx.snap deleted file mode 100644 index e8c4b0a310..0000000000 --- a/__tests__/shared/components/Contentful/TracksFilter/TracksAuthor/__snapshots__/TracksAuthor.jsx.snap +++ /dev/null @@ -1,25 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
- - Author - - -
-`; diff --git a/__tests__/shared/components/Contentful/TracksFilter/TracksDate/TracksDate.jsx b/__tests__/shared/components/Contentful/TracksFilter/TracksDate/TracksDate.jsx deleted file mode 100644 index 08f91606aa..0000000000 --- a/__tests__/shared/components/Contentful/TracksFilter/TracksDate/TracksDate.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { TracksDateInner } from 'components/Contentful/TracksFilter/TracksDate'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/TracksFilter/TracksDate/__snapshots__/TracksDate.jsx.snap b/__tests__/shared/components/Contentful/TracksFilter/TracksDate/__snapshots__/TracksDate.jsx.snap deleted file mode 100644 index 0978c0bdd8..0000000000 --- a/__tests__/shared/components/Contentful/TracksFilter/TracksDate/__snapshots__/TracksDate.jsx.snap +++ /dev/null @@ -1,44 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
- - Date - - - - - - - - - -
-`; diff --git a/__tests__/shared/components/Contentful/TracksFilter/TracksFilter.jsx b/__tests__/shared/components/Contentful/TracksFilter/TracksFilter.jsx deleted file mode 100644 index 6e7b09b0ae..0000000000 --- a/__tests__/shared/components/Contentful/TracksFilter/TracksFilter.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { TracksFilterInner } from 'components/Contentful/TracksFilter/TracksFilter'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - startDate: '2019-08-01T07:21:20.249Z', - endDate: '2019-09-01T07:21:20.249Z', - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/TracksFilter/TracksTags/TracksTags.jsx b/__tests__/shared/components/Contentful/TracksFilter/TracksTags/TracksTags.jsx deleted file mode 100644 index 328e5ade34..0000000000 --- a/__tests__/shared/components/Contentful/TracksFilter/TracksTags/TracksTags.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import React from 'react'; -import Renderer from 'react-test-renderer/shallow'; -import { TracksTagsInner } from 'components/Contentful/TracksFilter/TracksTags'; - -test('Matches shallow shapshot', () => { - const renderer = new Renderer(); - const MOCK_PROPS = { - theme: {}, - }; - renderer.render(); - expect(renderer.getRenderOutput()).toMatchSnapshot(); -}); diff --git a/__tests__/shared/components/Contentful/TracksFilter/TracksTags/__snapshots__/TracksTags.jsx.snap b/__tests__/shared/components/Contentful/TracksFilter/TracksTags/__snapshots__/TracksTags.jsx.snap deleted file mode 100644 index 62182b0ff3..0000000000 --- a/__tests__/shared/components/Contentful/TracksFilter/TracksTags/__snapshots__/TracksTags.jsx.snap +++ /dev/null @@ -1,19 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
- - Tags - -
- -
-
-`; diff --git a/__tests__/shared/components/Contentful/TracksFilter/__snapshots__/TracksFilter.jsx.snap b/__tests__/shared/components/Contentful/TracksFilter/__snapshots__/TracksFilter.jsx.snap deleted file mode 100644 index a16029d305..0000000000 --- a/__tests__/shared/components/Contentful/TracksFilter/__snapshots__/TracksFilter.jsx.snap +++ /dev/null @@ -1,74 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Matches shallow shapshot 1`] = ` -
-
- - filter - - -
- -
- - -
-
- - -
-
-`; diff --git a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap index 0436f05920..65b3670dee 100644 --- a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap @@ -143,7 +143,7 @@ exports[`Default render 1`] = ` "separator": true, }, Object { - "href": "/thrive/tracks?track=Topcoder", + "href": "https://community-app.topcoder-dev.com/thrive/tracks?track=Topcoder&tax=Help%20Articles", "title": "Help", }, Object { diff --git a/__tests__/shared/components/__snapshots__/TopcoderFooter.jsx.snap b/__tests__/shared/components/__snapshots__/TopcoderFooter.jsx.snap index 6b3d2bb144..10310b5e75 100644 --- a/__tests__/shared/components/__snapshots__/TopcoderFooter.jsx.snap +++ b/__tests__/shared/components/__snapshots__/TopcoderFooter.jsx.snap @@ -303,6 +303,15 @@ exports[`Matches shallow shapshot 1`] = ` About Community +
  • + + Changelog + +
  • diff --git a/__tests__/shared/components/challenge-listing/Filters/__snapshots__/ChallengeFilters.jsx.snap b/__tests__/shared/components/challenge-listing/Filters/__snapshots__/ChallengeFilters.jsx.snap index 477faa6ee6..5a29b8846c 100644 --- a/__tests__/shared/components/challenge-listing/Filters/__snapshots__/ChallengeFilters.jsx.snap +++ b/__tests__/shared/components/challenge-listing/Filters/__snapshots__/ChallengeFilters.jsx.snap @@ -9,6 +9,7 @@ exports[`Matches shallow shapshot shapshot 1`] = ` > + + ⨯ + + + ⨯ + THRIVE poll bridge -// pollArticlesForThrive(); - global.atob = atob; const CMS_BASE_URL = `https://app.contentful.com/spaces/${config.SECRET.CONTENTFUL.SPACE_ID}`; diff --git a/src/server/routes/contentful.js b/src/server/routes/contentful.js index 463310255d..638ff7bd02 100644 --- a/src/server/routes/contentful.js +++ b/src/server/routes/contentful.js @@ -2,7 +2,6 @@ * The routes that expose assets and content from Contentful CMS to the CDN. */ -import config from 'config'; import express from 'express'; import { @@ -13,16 +12,14 @@ import { articleVote, } from '../services/contentful'; -const routes = express.Router(); +const cors = require('cors'); -const LOCAL_MODE = Boolean(config.CONTENTFUL.LOCAL_MODE); +const routes = express.Router(); -/* Sets Access-Control-Allow-Origin header to avoid CORS error. - * TODO: Replace the wildcard value by an appropriate origin filtering. */ -routes.use((req, res, next) => { - res.set('Access-Control-Allow-Origin', '*'); - next(); -}); +// Enables CORS on those routes according config above +// ToDo configure CORS for set of our trusted domains +routes.use(cors()); +routes.options('*', cors()); /* Gets non-image asset file. */ routes.use( @@ -60,7 +57,7 @@ routes.use( routes.use('/:spaceName/:environment/preview/assets/:id', (req, res, next) => { const { environment, id, spaceName } = req.params; getService(spaceName, environment, true) - .getAsset(id, !LOCAL_MODE) + .getAsset(id) .then(res.send.bind(res), next); }); @@ -68,7 +65,7 @@ routes.use('/:spaceName/:environment/preview/assets/:id', (req, res, next) => { routes.use('/:spaceName/:environment/preview/assets', (req, res, next) => { const { environment, spaceName } = req.params; getService(spaceName, environment, true) - .queryAssets(req.query, !LOCAL_MODE) + .queryAssets(req.query) .then(res.send.bind(res), next); }); @@ -94,7 +91,7 @@ routes.use( (req, res, next) => { const { environment, id, spaceName } = req.params; getService(spaceName, environment, false) - .getAsset(id, !LOCAL_MODE) + .getAsset(id) .then(res.send.bind(res), next); }, ); @@ -103,7 +100,7 @@ routes.use( routes.use(':spaceName/:environment/published/assets', (req, res, next) => { const { environment, spaceName } = req.params; getService(spaceName, environment, false) - .queryAssets(req.query, !LOCAL_MODE) + .queryAssets(req.query) .then(res.send.bind(res), next); }); @@ -132,25 +129,4 @@ routes.use('/:spaceName/:environment/votes', (req, res, next) => { .then(res.send.bind(res), next); }); -/* Returns index of assets and content. */ -/* -routes.use('/index', async (req, res, next) => { - try { - res.set('Cache-Control', `max-age=${1000}`); - res.send(await getIndex()); - } catch (err) { next(err); } -}); -*/ - -/* Returns URL for the next sync of assets and content index with Contentful - * API. */ -/* -routes.use('/next-sync-url', async (req, res, next) => { - try { - res.set('Cache-Control', `max-age=${1000}`); - res.send(await getNextSyncUrl()); - } catch (err) { next(err); } -}); -*/ - export default routes; diff --git a/src/server/services/contentful.js b/src/server/services/contentful.js index 8dba5cf7e2..431ceda155 100644 --- a/src/server/services/contentful.js +++ b/src/server/services/contentful.js @@ -1,76 +1,35 @@ -/* eslint-disable no-param-reassign */ /** - * Server-side functions necessary for effective integration with Contentful - * CMS. + * Server-side functions necessary for effective integration + * with Contentful CMS */ import _ from 'lodash'; import config from 'config'; -import fetch from 'isomorphic-fetch'; -// import logger from 'utils/logger'; -// import moment from 'moment'; -import qs from 'qs'; +import { createClient } from 'contentful'; import { logger } from 'topcoder-react-lib'; +import { isomorphy } from 'topcoder-react-utils'; const contentful = require('contentful-management'); -const xml2json = require('xml2json'); /* Holds Contentful CDN URL. */ const CDN_URL = 'https://cdn.contentful.com/spaces'; -/* Holds the maximal index age [ms]. - * - * Set to 1 minute, which means ~100k API requests to Contentful from our dev - * and prod environments (preview API calls apart, but there should be not that - * many of them, as the circle of potential editors is edit, compared to that of - * the regular website visitors). - */ -// const INDEX_MAXAGE = 60 * 1000; - /* Holds Contentful Preview URL. */ const PREVIEW_URL = 'https://preview.contentful.com/spaces'; -/* Holds base URL of Community App CDN. */ -// const TC_CDN_URL = `${config.CDN.PUBLIC}/contentful`; - export const ASSETS_DOMAIN = 'assets.ctfassets.net'; export const IMAGES_DOMAIN = 'images.ctfassets.net'; -const MAX_FETCH_RETRIES = 5; - -/* GENERAL-PURPOSE CONTETNFUL API SERVICE. */ - /** - * Given an asset object, replaces its original file URL by URL leading to our - * own CloudFront CDN. - * - * BEWARE: - * - It mutates the argument asset. - * - * @param {Object} asset + * Generic logger for errors and warnings + * from Contentful API calls + * @param {String} level + * @param {String} data */ -/* -function mapAssetFileUrlToCdn(asset) { - let x = asset.fields.file.url.split('/'); - switch (x[2]) { - case ASSETS_DOMAIN: - x = `${TC_CDN_URL}/assets/${x[4]}/${x[5]}/${x[6]}`; - break; - case IMAGES_DOMAIN: - x = `${TC_CDN_URL}/images/${x[4]}/${x[5]}/${x[6]}`; - break; - default: throw new Error('Unexpected asset location'); +function logHandler(level, data) { + if (isomorphy.isDev) { + logger.log('Contentful logHandler', level, data); } - asset.fields.file.url = x; // eslint-disable-line no-param-reassign -} -*/ - -/** - * Creates a promise that resolves two second after its creation. - * @return {Promise} - */ -function oneSecondDelay() { - return new Promise(resolve => setTimeout(resolve, 1000)); } /** @@ -82,54 +41,32 @@ class ApiService { * Creates a new service instance. * @param {String} baseUrl The base API endpoint. * @param {String} key API key. + * @param {String} spaceId The space id. + * @param {Boolean} preview Use the preview API? */ - constructor(baseUrl, key) { - this.private = { baseUrl, key }; - } - - /** - * Gets data from the specified endpoing. - * @param {String} endpoint - * @param {Object} query Optional. URL query to append to the request. - * @return {Promise} - */ - async fetch(endpoint, query) { - // Contentful API rate limits, which are 78 requests within 1 second - // we await 14ms before each request to make sure we don't break this limitation - await new Promise(resolve => setTimeout(resolve, 14)); - // fire calls to Contentful API - let url = `${this.private.baseUrl}${endpoint}`; - if (query) url += `?${qs.stringify(query)}`; - let res; - for (let i = 0; i < MAX_FETCH_RETRIES; i += 1) { - /* The loop is here to retry async operation multiple times in case of - * failures due to violation of Contentful API rate limits, which are - * 78 requests within 1 second. Thus, it is a valid use of await inside - * loop. */ - /* eslint-disable no-await-in-loop */ - res = await fetch(url, { - headers: { Authorization: `Bearer ${this.private.key}` }, - }); - /* 429 = "Too Many Requests" */ - if (res.status !== 429) break; - await oneSecondDelay(); - /* eslint-enable no-await-in-loop */ - } - if (!res.ok) throw new Error(res.statusText); - return res.json(); + constructor(baseUrl, key, spaceId, preview) { + this.private = { + baseUrl, key, spaceId, preview, + }; + // client config + const clientConf = { + accessToken: key, + space: spaceId, + logHandler, + }; + if (preview) clientConf.host = 'preview.contentful.com'; + // create the client to work with + this.client = createClient(clientConf); } /** * Gets the specified asset. * @param {String} id Asset ID. - * @param {Boolean} mapFileUrlToCdn Optional. Pass in `true` to replace the - * actual file path by Community App CDN path. * @return {Promise} */ - async getAsset(id /* , mapFileUrlToCdn */) { - const res = await this.fetch(`/assets/${id}`); - // if (mapFileUrlToCdn) mapAssetFileUrlToCdn(res); - return res; + async getAsset(id) { + const res = await this.client.getAsset(id); + return res.stringifySafe ? JSON.parse(res.stringifySafe()) : res; } /** @@ -137,21 +74,19 @@ class ApiService { * @param {String} id Entry ID. * @return {Promise} */ - getEntry(id) { - return this.fetch(`/entries/${id}`); + async getEntry(id) { + const res = await this.client.getEntry(id); + return res.stringifySafe ? JSON.parse(res.stringifySafe()) : res; } /** * Queries assets. * @param {Object} query Optional. Query. - * @param {Boolean} mapFileUrlToCdn Optional. Pass in `true` to replace the - * actual file path by Community App CDN path. * @return {Promise} */ - async queryAssets(query /* , mapFileUrlToCdn */) { - const res = await this.fetch('/assets', query); - // if (mapFileUrlToCdn) res.items.forEach(x => mapAssetFileUrlToCdn(x)); - return res; + async queryAssets(query) { + const res = await this.client.getAssets(query); + return res.stringifySafe ? JSON.parse(res.stringifySafe()) : res; } /** @@ -159,8 +94,9 @@ class ApiService { * @param {Object} query Optional. Query for filtering / sorting of entries. * @return {Promise} */ - queryEntries(query) { - return this.fetch('/entries', query); + async queryEntries(query) { + const res = await this.client.getEntries(query); + return res.stringifySafe ? JSON.parse(res.stringifySafe()) : res; } } @@ -179,17 +115,21 @@ export function articleVote(body) { .then(environment => environment.getEntry(body.id)) .then((entry) => { if (!entry.fields.upvotes) { + // eslint-disable-next-line no-param-reassign entry.fields.upvotes = { 'en-US': body.votes.upvotes, }; } else { + // eslint-disable-next-line no-param-reassign entry.fields.upvotes['en-US'] = body.votes.upvotes; } if (!entry.fields.downvotes) { + // eslint-disable-next-line no-param-reassign entry.fields.downvotes = { 'en-US': body.votes.downvotes, }; } else { + // eslint-disable-next-line no-param-reassign entry.fields.downvotes['en-US'] = body.votes.downvotes; } return entry.update(); @@ -197,136 +137,12 @@ export function articleVote(body) { .then(entry => entry.publish()); } -/** - * This function fetches TC RSS feed to create draft articles in THRIVE for the new posted blogs. - * It calls itself by interval to poll for new blogs. - * Runs on server side only. - */ -export async function pollArticlesForThrive() { - // make this function execute only when in production - if (process.env.BABEL_ENV !== 'production') return; - logger.log('polling blog articles for THRIVE -> INIT'); - // Create Contentful client to work with - const client = contentful.createClient({ - accessToken: config.SECRET.CONTENTFUL.MANAGEMENT_TOKEN, - }); - // connect to Thrive space - const env = await client.getSpace(config.SECRET.CONTENTFUL.EDU.SPACE_ID) - .then(space => space.getEnvironment('master')); - // fetch the poll interval - const pollConf = await env.getEntry('7n1HT3MhUhgjvCzd36Nymd'); - const pollInt = pollConf ? pollConf.fields.timeInSeconds : 86400; // default to day if not found - // fetch the RSS feed and parse it - const feedXML = await fetch(config.URL.THRIVE_POLL_FEED); - if (feedXML.ok) { - let feed = await feedXML.text(); - feed = await Promise.resolve(xml2json.toJson(feed, { object: true })); - if (feed) { - // when feed loaded and parsed ok - // apply filter first based on tags work only with 'Community Stories' - feed.rss.channel.item = _.filter( - feed.rss.channel.item, - blogItem => blogItem.category.indexOf('Community Stories') !== -1, - ); - // loop all feed items and check if those exists in space - // if not existing then create - Promise.all( - _.map(feed.rss.channel.item, - blogPost => env.getEntries({ - content_type: 'article', - 'fields.title[match]': blogPost.title, - }) - .then((queryData) => { - if (queryData.total === 0) { - // article not found in Contentful space - // will create it with payload... - const article = { - fields: { - title: { 'en-US': blogPost.title }, - type: { 'en-US': 'Article' }, - tags: { 'en-US': blogPost.category }, - creationDate: { 'en-US': new Date(blogPost.pubDate) }, - content: { 'en-US': blogPost.description }, - externalArticle: { 'en-US': true }, - contentUrl: { 'en-US': blogPost.link }, - }, - }; - // check if author exists - // if yes link it if no create it? - return env.getEntries({ - content_type: 'person', - query: blogPost['dc:creator'], - }) - .then((author) => { - // found an author that matches link it - if (author.total) { - article.fields.contentAuthor = { - 'en-US': [{ - sys: { - type: 'Link', linkType: 'Entry', id: author.items[0].sys.id, - }, - }], - }; - } - // try get the page to extract its featured image - return fetch(blogPost.link) - .then(rsp => rsp.text()) - .then((HTMLsrc) => { - const match = /]*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 ? Member Portait : }
  • -

    +

    {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 {