Skip to content

Commit 8c16c11

Browse files
committed
Fixes for challenge cards in challnege listing
- Fixes #239, fixes #240 - Refactors ChallengeCard component (splits out NumRegistrants and NumSubmissions components, with related logic, into separate components). - Improves How To Deep-Link doc.
1 parent 0477571 commit 8c16c11

File tree

6 files changed

+332
-227
lines changed

6 files changed

+332
-227
lines changed

docs/how-to-deep-link.md

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ For some page you want to expose its state (or part of the state) via URL query
66
- When that URL is used to access the app, the page is opened with the state described by the URL query params.
77

88
### Solution
9-
The key idea is simple: internally, you still use Redux actions / reducers / store to describe and modify entire state of the page. Inside reducer's action handlers, related to that parts of the state, that you want to expose via URL query, you add the code to properly update URL each time reducer re-evaluates related pieces of the state. Inside that reducer's factory you, in case of server-side rendering, check query parameters of HTTP request and create proper intial state of Redux store. To navigate around the app / change the page state from within the app, you just dispatch related actions, URL will be automatically updated by reducers. **In the cases when you programmatically change the route inside the app, using react-router, related actions should be dispatched after transition, as explained below**.
9+
The key idea is simple: internally, you still use Redux actions / reducers / store to describe and modify entire state of the page. Inside reducer's action handlers, related to that parts of the state, that you want to expose via URL query, you add the code to properly update URL each time reducer re-evaluates related pieces of the state. Inside that reducer's factory you, in case of server-side rendering, check query parameters of HTTP request and create proper intial state of Redux store. To change the page state without transition between pages, you just dispatch related actions, the URL will be automatically updated. **In the cases when you programmatically change the route inside the app, using react-router, related actions should be dispatched after transition, as explained below**.
1010

1111
### Examples
1212
At the moment of writing this instruction, this approach is used inside challenge listings to expose challenge filters and selected buckets via URL query; and also within challenge details page to expose different detail tabs. The implementation notes below will refer to the second usage.
@@ -43,19 +43,24 @@ At the moment of writing this instruction, this approach is used inside challeng
4343

4444
- To make a transition to the page, from another route within the app, and select the desired tab you:
4545
- As usually use `<Link>` component from `react-router` to make transition (in our codebase we have an auxiliary wrapper around it in [`utils/router` module](https://github.com/topcoder-platform/community-app/blob/develop/src/shared/utils/router/index.jsx), also the standard [buttons](https://github.com/topcoder-platform/community-app/tree/develop/src/shared/components/buttons) and [tags](https://github.com/topcoder-platform/community-app/tree/develop/src/shared/components/tags) are rendered as `react-router` `<Link>`s when appropriate; the idea stay the same if you use it);
46-
- In `to` prop you pass the target endpoint without query params (specifying query params within this prop makes no sense: it will update the URL, but won't update Redux state properly).
47-
- Inside `onClick` prop you dispatch necessary action(s), wrapped inside `setImmediate(..)` method. For example, to move from the challenge listing to the challenge details page of a challenge with ID 12345, and with winners tab open (provided that the challenge is completed and thus it is appopriate to link to that tab) you do something like this:
48-
```jsx
49-
<Link
50-
onClick={() =>
51-
setImmediate(() => selectTab('winners'))
52-
}
53-
to="/challenges/12345"
54-
>Winners</Link>
55-
```
56-
where `selectTab(..)` function is mapped to the corresponding action within page (component) container.
57-
58-
`setImmediate(..)` is necessary here to delay update Redux store after the endpoint transition is handled by `react-redux`; because of `to="/challenges/12345"` `react-redux` will remove any query params from URL. The `onClick` callback, when wrapped inside `setImmediate(..)` will be triggered right after the transition, it will properly update Redux state, and set the correct URL query. From visitor's point of view it happens with no pause in beween (i.e. when the page is loaded he already will see the target tab).
46+
47+
- `to` prop of `<Link>` specifies target route (and query params, if specified) for `react-router`. It does not update Redux state, so you should also supply `onClick` prop, which will dispatch all necessary actions:
48+
```js
49+
<Link
50+
onClick={() => selectTab('winners')}
51+
to="/challenges/12345?tab=winners"
52+
>
53+
```
54+
where `selectTab(..)` function is mapped to the corresponding action within page (component) container.
55+
56+
- It is important to note that in the code above, transition between the routes is handled by `react-router` after the moment `selectTab(..)` is triggered and handled by reducers; thus, the query params written to URL by reducers will be overriden by those you specify inside `to` prop. It means, if you make a mistake and provide a wrong query there, it will be out of sync with the actual state of the page after transition. As an alternative, you can do
57+
```js
58+
<Link
59+
onClick={() => setImmediate(() => selectTab('winners'))}
60+
to="/challenges/12345?tab=winners"
61+
>
62+
```
63+
In this case `selectTab(..)` will be triggered after transition, thus reducers will take care about proper query params written in URL. However, you still want to leave correct query inside `to`, because when user copies a link with right mouse button, or open it in a new page, he will get the URL specified there there.
5964

6065
### Caveats
6166
- When you read url query params at the server side, any array with 20 and more elements will be parsed as an object with keys equal to array element indices. Under the hood it is done by [qs](https://www.npmjs.com/package/qs) module on purpose. Don't try to reconfigure `qs`, just remember that you can get an object when you expect an array, and handle that situation correctly.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Registrants icon with count of registrants. Acts as the link to Registrants
3+
* tab of Challenge Details page (to Details tab, when there is no registrants
4+
* in the challenge yet). Shows a tooltip when hovered.
5+
*/
6+
7+
import _ from 'lodash';
8+
import config from 'utils/config';
9+
import PT from 'prop-types';
10+
import React from 'react';
11+
import Tooltip from 'components/Tooltip';
12+
import { DETAIL_TABS } from 'actions/challenge';
13+
import { Link } from 'utils/router';
14+
15+
/* TODO: The icon should be converted back to SVG and imported using the
16+
* the standard approach for our code! */
17+
import RegistrantsIcon from '../../Icons/RegistrantsIcon';
18+
19+
import './style.scss';
20+
21+
const ID_LENGTH = 6;
22+
const MM_BASE_URL
23+
= `${config.URL.COMMUNITY}/longcontest/?module=ViewRegistrants&rd=`;
24+
25+
export default function NumRegistrants({
26+
challenge: { id, numRegistrants, track },
27+
selectChallengeDetailsTab,
28+
}) {
29+
let tip;
30+
switch (numRegistrants) {
31+
case 0: tip = 'No registrants'; break;
32+
case 1: tip = '1 total registrant'; break;
33+
default: tip = `${numRegistrants} total registrants`;
34+
}
35+
const query = numRegistrants ? `?tab=${DETAIL_TABS.REGISTRANTS}` : '';
36+
const link = track === 'DATA_SCIENCE' && _.toString(id).length < ID_LENGTH
37+
? `${MM_BASE_URL}${id}` : `/challenges/${id}${query}`;
38+
return (
39+
<span styleName="container">
40+
<Tooltip
41+
content={
42+
<div styleName="tooltip">{tip}</div>
43+
}
44+
>
45+
<Link
46+
onClick={() => (
47+
numRegistrants
48+
? selectChallengeDetailsTab(DETAIL_TABS.REGISTRANTS) : null
49+
)}
50+
to={link}
51+
>
52+
<RegistrantsIcon />
53+
<span styleName="number">{numRegistrants}</span>
54+
</Link>
55+
</Tooltip>
56+
</span>
57+
);
58+
}
59+
60+
NumRegistrants.propTypes = {
61+
challenge: PT.shape({
62+
id: PT.oneOfType([PT.number, PT.string]).isRequired,
63+
numRegistrants: PT.number.isRequired,
64+
track: PT.string.isRequired,
65+
}).isRequired,
66+
selectChallengeDetailsTab: PT.func.isRequired,
67+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@import "~styles/tc-styles";
2+
3+
.container {
4+
display: inline-block;
5+
font-weight: 400;
6+
margin-left: 4 * $base-unit;
7+
8+
@include xxs-to-xs {
9+
margin-left: 3 * $base-unit;
10+
}
11+
}
12+
13+
.number {
14+
vertical-align: top;
15+
padding-left: $base-unit - 3;
16+
}
17+
18+
.tooltip {
19+
font-weight: 500;
20+
font-size: 13px;
21+
color: $tc-white;
22+
letter-spacing: 0;
23+
padding: 15px;
24+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/**
2+
* Submissions icon with count of submissions. Acts as the link to Submissions
3+
* tab of Challenge Details page (to Details tab, when there is no registrants
4+
* in the challenge yet). Shows a tooltip when hovered.
5+
*/
6+
7+
import _ from 'lodash';
8+
import config from 'utils/config';
9+
import PT from 'prop-types';
10+
import React from 'react';
11+
import Tooltip from 'components/Tooltip';
12+
import { DETAIL_TABS } from 'actions/challenge';
13+
import { Link } from 'utils/router';
14+
15+
/* TODO: The icon should be converted back to SVG and imported using the
16+
* the standard approach for our code! */
17+
import SubmissionsIcon from '../../Icons/SubmissionsIcon';
18+
19+
import './style.scss';
20+
21+
const ID_LENGTH = 6;
22+
const MM_BASE_URL
23+
= `${config.URL.COMMUNITY}/longcontest/?module=ViewStandings&rd=`;
24+
25+
export default function NumSubmissions({
26+
challenge: { id, numSubmissions, track },
27+
selectChallengeDetailsTab,
28+
}) {
29+
let tip;
30+
switch (numSubmissions) {
31+
case 0: tip = 'No submissions'; break;
32+
case 1: tip = '1 total submission'; break;
33+
default: tip = `${numSubmissions} total submissions`;
34+
}
35+
const query = numSubmissions ? `?tab=${DETAIL_TABS.SUBMISSIONS}` : '';
36+
const link = track === 'DATA_SCIENCE' && _.toString(id).length < ID_LENGTH
37+
? `${MM_BASE_URL}${id}` : `/challenges/${id}${query}`;
38+
return (
39+
<div styleName="container">
40+
<Tooltip
41+
content={
42+
<div styleName="tooltip">{tip}</div>
43+
}
44+
>
45+
<Link
46+
onClick={() => (
47+
numSubmissions
48+
? selectChallengeDetailsTab(DETAIL_TABS.SUBMISSIONS) : null
49+
)}
50+
to={link}
51+
>
52+
<SubmissionsIcon />
53+
<span styleName="number">{numSubmissions}</span>
54+
</Link>
55+
</Tooltip>
56+
</div>
57+
);
58+
}
59+
60+
NumSubmissions.propTypes = {
61+
challenge: PT.shape({
62+
id: PT.oneOfType([PT.number, PT.string]).isRequired,
63+
numSubmissions: PT.number.isRequired,
64+
track: PT.string.isRequired,
65+
}).isRequired,
66+
selectChallengeDetailsTab: PT.func.isRequired,
67+
};
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@import "~styles/tc-styles";
2+
3+
.container {
4+
display: inline-block;
5+
font-weight: 400;
6+
margin-left: 4 * $base-unit;
7+
8+
@include xxs-to-xs {
9+
margin-left: 3 * $base-unit;
10+
}
11+
}
12+
13+
.number {
14+
vertical-align: top;
15+
padding-left: $base-unit - 3;
16+
}
17+
18+
.tooltip {
19+
font-weight: 500;
20+
font-size: 13px;
21+
color: $tc-white;
22+
letter-spacing: 0;
23+
padding: 15px;
24+
}

0 commit comments

Comments
 (0)