Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 14 additions & 7 deletions __tests__/shared/actions/challenge.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import actions from 'actions/challenge';

jest.mock('services/challenges');

const mockFetch = resolvesTo => jest.fn(() =>
Promise.resolve({ json: () => resolvesTo }));

Expand Down Expand Up @@ -51,13 +53,18 @@ describe('challenge.getDetailsDone', () => {
expect(a.type).toBe('CHALLENGE/GET_DETAILS_DONE');
});

/* TODO: This test does not work anymore, as the action was refactored to
* use challenges service for API v3 calls, while API v2 calls still happen
* the way they used to. Thus, we need a better mock of fetch to test it,
* or some alternative approach. */
test.skip('payload is a promise which resolves to the expected object', () =>
a.payload.then(res => expect(res).toEqual(
['DUMMY DATA', { result: { content: ['DUMMY DATA'] } }, {}])));
const mockChallenge =
require('services/__mocks__/data/challenges-v3.json').result.content[0];

test('payload is a promise which resolves to the expected object', () =>
a.payload.then(res => expect(res).toEqual([
mockChallenge, {
result: {
content: ['DUMMY DATA'],
},
}, undefined,
])),
);
});


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,15 +131,14 @@ exports[`Snapshot match 2`] = `
<div
className="tc-communities__header__logos src-shared-components-tc-communities-Header-___style__logos___38nqG"
>
<Connect(RRLinkWrapper)
<span
className="tc-communities__header__logo src-shared-components-tc-communities-Header-___style__logo___z2xG6"
to="pageId1"
>
<img
alt="Community logo"
alt="logo"
src="some/logo/url"
/>
</Connect(RRLinkWrapper)>
</span>
</div>
<div
className="tc-communities__header__challenge-dropdown src-shared-components-tc-communities-Header-___style__challenge-dropdown___3fQW9"
Expand Down
9 changes: 8 additions & 1 deletion docs/how-to-add-a-new-topcoder-community.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,14 @@ To add a new community with the name **demo**, we should follow the following pr
- `communitySelector` - *Object Array* - Specifies data for the community selection dropdown inside the community header. Each object MUST HAVE `label` and `value` string fields, and MAY HAVE `redirect` field. If `redirect` field is specified, a click on that option in the dropdown will redirect user to the specified URL.
- `groupId` - *String* - This value of group ID is now used to fetch community statistics. Probably, it makes sense to use this value everywhere where `authorizedGroupIds` array is used, however, at the moment, these two are independent.
- `leaderboardApiUrl` - *String* - Endpoint from where the leaderboard data should be loaded.
- `logo` - *String Array* - Array of image URLs to insert as logos into the left corner of community's header.
- `logos` - *String Array | Object Array* - Array of image URLs to insert as logos into the left corner of community's header, alternatively the array may contain JS objects of shape
```
{
"img": "<SOME-IMAGE-URL>",
"url": "https://www.topcoder.com"
}
```
For such elements `img` will be used as the image source, and `url` will be the redirection URL triggered by a click on the logo.
- `additionalLogos` - *String Array* - Array of image URLs to insert as logos into the right corner of community's header.
- `hideSearch` - *Boolean* - Hide/Show the search icon.
- `chevronOverAvatar` - *Boolean* - Render a *chevron-down* instead of the user avatar.
Expand Down
8 changes: 4 additions & 4 deletions src/server/tc-communities/community-2/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
}],
"groupId": "20000002",
"leaderboardApiUrl": "https://api.topcoder.com/v4/looks/458/run/json/",
"logos": [
"/themes/community-2/wipro-logo.png",
"/themes/community-2/logo_topcoder_with_name.svg"
],
"logos": [{
"img": "/themes/community-2/logo_topcoder_with_name.svg",
"url": "https://www.topcoder.com"
}],
"menuItems": [
{
"title": "Home",
Expand Down
7 changes: 4 additions & 3 deletions src/server/tc-communities/demo-expert/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
"value": "3"
}],
"groupId": "1001",
"logos": [
"/themes/demo-expert/logo_topcoder_with_name.svg"
],
"logos": [{
"img": "/themes/demo-expert/logo_topcoder_with_name.svg",
"url": "https://www.topcoder.com"
}],
"menuItems": [
{
"title": "Home",
Expand Down
7 changes: 4 additions & 3 deletions src/server/tc-communities/taskforce/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@
}],
"groupId": "20000003",
"leaderboardApiUrl": "https://api.topcoder.com/v4/looks/458/run/json/",
"logos": [
"/themes/taskforce/logo_topcoder_with_name.svg"
],
"logos": [{
"img": "/themes/taskforce/logo_topcoder_with_name.svg",
"url": "https://www.topcoder.com"
}],
"menuItems": [
{
"title": "Home",
Expand Down
7 changes: 4 additions & 3 deletions src/server/tc-communities/tc-prod-dev/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
"value": "3"
}],
"groupId": "20000001",
"logos": [
"/themes/wipro/logo_topcoder_with_name.svg"
],
"logos": [{
"img": "/themes/wipro/logo_topcoder_with_name.svg",
"url": "https://www.topcoder.com"
}],
"menuItems": [
{
"title": "Home",
Expand Down
19 changes: 9 additions & 10 deletions src/server/tc-communities/wipro/metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,19 @@
"label": "TopGear Community",
"value": "1"
}, {
"label": "Cognitive Community",
"redirect": "http://cognitive.topcoder.com/",
"label": "Topcoder Public Community",
"redirect": "https://www.topcoder.com",
"value": "2"
}, {
"label": "iOS Community",
"redirect": "https://ios.topcoder.com/",
"value": "3"
}],
"groupId": "20000000",
"leaderboardApiUrl": "https://api.topcoder.com/v4/looks/458/run/json/",
"logos": [
"/themes/wipro/wipro-logo.png",
"/themes/wipro/logo_topcoder_with_name.svg"
],
"logos": [{
"img": "/themes/wipro/wipro-logo.png",
"url": "http://www.wipro.com/"
}, {
"img": "/themes/wipro/logo_topcoder_with_name.svg",
"url": "https://www.topcoder.com"
}],
"additionalLogos": [
"/themes/wipro/topgear_logo.png"
],
Expand Down
45 changes: 25 additions & 20 deletions src/shared/components/tc-communities/Header/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,30 @@ export default function Header(props) {
};
}

const renderedLogos = logos.map((item) => {
const img = _.isString(item) ? item : item.img;
let logo = <img alt="logo" src={_.isString(item) ? item : item.img} />;
if (_.isObject(item) && item.url) {
logo = (
<Link
key={img}
to={item.url}
styleName="logo"
className="tc-communities__header__logo"
>{logo}</Link>
);
} else {
logo = (
<span
key={img}
styleName="logo"
className="tc-communities__header__logo"
>{logo}</span>
);
}
return logo;
});

const loginState = profile ? (
<div
onMouseEnter={event => openMenu(userSubMenu, event.target)}
Expand Down Expand Up @@ -127,26 +151,7 @@ export default function Header(props) {
</button>
<div styleName="logos-wrap">
<div styleName="logos" className="tc-communities__header__logos">
{_.map(logos, (logoUrl, index) =>
(menuItems.length ? (
<Link
key={index}
to={menuItems[0].url}
styleName="logo"
className="tc-communities__header__logo"
>
<img src={logoUrl} alt="Community logo" />
</Link>
) : (
<span
key={index}
styleName="logo"
className="tc-communities__header__logo"
>
<img src={logoUrl} alt="Community logo" />
</span>
)),
)}
{renderedLogos}
</div>

<div
Expand Down
138 changes: 138 additions & 0 deletions src/shared/services/__mocks__/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/**
* This module provides a service for conventient access to Topcoder APIs.
*/

import _ from 'lodash';
import 'isomorphic-fetch'; /* global fetch */
import config from 'utils/config';

/**
* API service object. It is reused for both Topcoder API v2 and v3,
* as in these cases we are fine with the same interface, and the only
* thing we need to be different is the base URL and auth token to use.
*/
export default class Api {
/**
* @param {String} base Base URL of the API.
* @param {String} token Optional. Authorization token.
*/
constructor(base, token) {
this.private = { base, token };
}

/**
* Sends a request to the specified endpoint of the API. This method just
* wraps fetch() in a convenient way. If this object was created with the
* auth token, it will be automatically added to auth header of all
* requests.
* For additional details see https://github.github.io/fetch/
* @param {String} enpoint Should start with slash, like /endpoint.
* @param {Object} options Optional. Fetch options.
* @return {Promise} It resolves to the HTTP response object. To get the
* actual data you probably want to call .json() method of that object.
* Mind that this promise rejects only on network errors. In case of
* HTTP errors (404, etc.) the promise will be resolved successfully,
* and you should check .status or .ok fields of the response object
* to find out the response status.
*/
fetch(endpoint, options) {
const p = this.private;
const headers = { 'Content-Type': 'application/json' };
if (p.token) headers.Authorization = `Bearer ${p.token}`;
const ops = _.merge(_.cloneDeep(options) || {}, { headers });
return fetch(`${p.base}${endpoint}`, ops);
}

/**
* Sends DELETE request to the specified endpoint.
* @param {String} endpoint
* @param {Blob|BufferSource|FormData|String} body
* @return {Promise}
*/
delete(endpoint, body) {
return this.fetch(endpoint, { body, method: 'DELETE' });
}

/**
* Sends GET request to the specified endpoint.
* @param {String} endpoint
* @return {Promise}
*/
get(endpoint) {
return this.fetch(endpoint);
}

/**
* Sends POST request to the specified endpoint.
* @param {String} endpoint
* @param {Blob|BufferSource|FormData|String} body
* @return {Promise}
*/
post(endpoint, body) {
return this.fetch(endpoint, { body, method: 'POST' });
}

/**
* Sends POST request to the specified endpoint, with JSON payload.
* @param {String} endpoint
* @param {JSON} json
* @return {Promise}
*/
postJson(endpoint, json) {
return this.post(endpoint, JSON.stringify(json));
}

/**
* Sends PUT request to the specified endpoint.
* @param {String} endpoint
* @param {Blob|BufferSource|FormData|String} body
* @return {Promise}
*/
put(endpoint, body) {
return this.fetch(endpoint, { body, method: 'PUT' });
}

/**
* Sends PUT request to the specified endpoint.
* @param {String} endpoint
* @param {JSON} json
* @return {Promise}
*/
putJson(endpoint, json) {
return this.put(endpoint, JSON.stringify(json));
}
}

/**
* Topcoder API v2.
*/

/**
* Returns a new or existing Api object for Topcoder API v2.
* @param {String} token Optional. Auth token for Topcoder API v2.
* @return {Api} API v2 service object.
*/
let lastApiV2 = null;
export function getApiV2(token) {
if (!lastApiV2 || lastApiV2.private.token !== token) {
lastApiV2 = new Api(config.API.V2, token);
}
return lastApiV2;
}

/**
* Topcoder API v3.
*/

/**
* Returns a new or existing Api object for Topcoder API v3
* @param {String} token Optional. Auth token for Topcoder API v3.
* @return {Api} API v3 service object.
*/
let lastApiV3 = null;
export function getApiV3(token) {
if (!lastApiV3 || lastApiV3.private.token !== token) {
lastApiV3 = new Api(config.API.V3, token);
}
return lastApiV3;
}
Loading