diff --git a/.exchange-rates.cache b/.exchange-rates.cache index 1c6395db7b..e322ecf138 100644 --- a/.exchange-rates.cache +++ b/.exchange-rates.cache @@ -1 +1 @@ -{"disclaimer":"Usage subject to terms: https://openexchangerates.org/terms","license":"https://openexchangerates.org/license","timestamp":1507798800,"base":"USD","rates":{"AED":3.672896,"AFN":67.974,"ALL":113,"AMD":478.925,"ANG":1.77865,"AOA":165.9215,"ARS":17.4045,"AUD":1.277432,"AWG":1.790501,"AZN":1.69,"BAM":1.648856,"BBD":2,"BDT":82.288,"BGN":1.64876,"BHD":0.377213,"BIF":1746.8,"BMD":1,"BND":1.355356,"BOB":6.885986,"BRL":3.171391,"BSD":1,"BTC":0.000195885465,"BTN":65.230321,"BWP":10.283758,"BYN":1.961895,"BZD":2.008642,"CAD":1.245347,"CDF":1562.881563,"CHF":0.973707,"CLF":0.02324,"CLP":626.965409,"CNH":6.583036,"CNY":6.585567,"COP":2953.31,"CRC":572.515,"CUC":1,"CUP":25.5,"CVE":93.375,"CZK":21.81801,"DJF":178.97,"DKK":6.275117,"DOP":47.278488,"DZD":113.454,"EGP":17.622,"ERN":15.333158,"ETB":23.45091,"EUR":0.842994,"FJD":2.051354,"FKP":0.75579,"GBP":0.75579,"GEL":2.478108,"GGP":0.75579,"GHS":4.395,"GIP":0.75579,"GMD":46.275,"GNF":8906.35,"GTQ":7.319909,"GYD":207.06,"HKD":7.80686,"HNL":23.331869,"HRK":6.330489,"HTG":63.080843,"HUF":260.717,"IDR":13502.802788,"ILS":3.4853,"IMP":0.75579,"INR":65.075,"IQD":1162.95,"IRR":33654.5,"ISK":104.74,"JEP":0.75579,"JMD":127.971482,"JOD":0.709001,"JPY":112.27,"KES":103.25,"KGS":68.399709,"KHR":4025.7,"KMF":415.25,"KPW":900,"KRW":1132.55,"KWD":0.301803,"KYD":0.830511,"KZT":333.96,"LAK":8315,"LBP":1502.9,"LKR":152.952819,"LRD":118.010556,"LSL":13.517836,"LYD":1.364414,"MAD":9.3903,"MDL":17.424728,"MGA":3046.65,"MKD":51.91,"MMK":1359.15,"MNT":2457.447826,"MOP":8.011891,"MRO":363.721657,"MUR":34.0005,"MVR":15.400126,"MWK":724.675734,"MXN":18.702241,"MYR":4.2195,"MZN":60.994761,"NAD":13.517351,"NGN":358.75,"NIO":30.207843,"NOK":7.9106,"NPR":103.905516,"NZD":1.404011,"OMR":0.385005,"PAB":1,"PEN":3.25875,"PGK":3.183202,"PHP":51.39,"PKR":104.782432,"PLN":3.600585,"PYG":5620.55,"QAR":3.729989,"RON":3.868701,"RSD":100.609575,"RUB":57.665,"RWF":830.479566,"SAR":3.750196,"SBD":7.810521,"SCR":13.541191,"SDG":6.653117,"SEK":8.092327,"SGD":1.353802,"SHP":0.75579,"SLL":7635.994028,"SOS":576.39,"SRD":7.448,"SSP":129.0194,"STD":20701.93181,"SVC":8.719665,"SYP":514.99499,"SZL":13.528287,"THB":33.105,"TJS":8.764406,"TMT":3.499986,"TND":2.463097,"TOP":2.247061,"TRY":3.637455,"TTD":6.70213,"TWD":30.197,"TZS":2245,"UAH":26.423235,"UGX":3617.4,"USD":1,"UYU":29.175341,"UZS":8032.8,"VEF":10.146563,"VND":22709.292617,"VUV":105.278798,"WST":2.531559,"XAF":552.967845,"XAG":0.05802155,"XAU":0.00077159,"XCD":2.70255,"XDR":0.707444,"XOF":552.967845,"XPD":0.0010379,"XPF":100.595948,"XPT":0.00106726,"YER":250.306642,"ZAR":13.460823,"ZMW":9.591214,"ZWL":322.355011}} \ No newline at end of file +{"disclaimer":"Usage subject to terms: https://openexchangerates.org/terms","license":"https://openexchangerates.org/license","timestamp":1508414400,"base":"USD","rates":{"AED":3.672973,"AFN":68.15,"ALL":112.92,"AMD":481.728804,"ANG":1.779789,"AOA":165.9225,"ARS":17.3379,"AUD":1.270644,"AWG":1.790246,"AZN":1.6985,"BAM":1.653896,"BBD":2,"BDT":82.537473,"BGN":1.653246,"BHD":0.377203,"BIF":1750,"BMD":1,"BND":1.357043,"BOB":6.909062,"BRL":3.167437,"BSD":1,"BTC":0.000175811708,"BTN":65.045224,"BWP":10.307664,"BYN":1.957114,"BZD":2.009704,"CAD":1.246334,"CDF":1570,"CHF":0.975172,"CLF":0.023185,"CLP":624.8,"CNH":6.619015,"CNY":6.6223,"COP":2923.75,"CRC":569.998642,"CUC":1,"CUP":25.5,"CVE":93.9,"CZK":21.7448,"DJF":178.77,"DKK":6.290249,"DOP":47.86869,"DZD":114.25,"EGP":17.6474,"ERN":15.191062,"ETB":26.984211,"EUR":0.844987,"FJD":2.046698,"FKP":0.758867,"GBP":0.758867,"GEL":2.482304,"GGP":0.758867,"GHS":4.405,"GIP":0.758867,"GMD":47.45,"GNF":9001,"GTQ":7.351488,"GYD":207.795851,"HKD":7.80255,"HNL":23.592243,"HRK":6.3449,"HTG":63.506926,"HUF":260.652005,"IDR":13513.746714,"ILS":3.496997,"IMP":0.758867,"INR":65.015,"IQD":1166.85,"IRR":34243.946723,"ISK":105.21,"JEP":0.758867,"JMD":127.415141,"JOD":0.7086,"JPY":112.49516667,"KES":103.585,"KGS":68.579481,"KHR":4035.5,"KMF":417.634177,"KPW":900,"KRW":1131.75,"KWD":0.302094,"KYD":0.833193,"KZT":334.519063,"LAK":8323.9,"LBP":1515.649713,"LKR":153.548466,"LRD":118.354791,"LSL":13.535513,"LYD":1.369746,"MAD":9.404986,"MDL":17.329616,"MGA":3082.1,"MKD":52.033759,"MMK":1367.3,"MNT":2454.299176,"MOP":8.035619,"MRO":355.495295,"MUR":33.95,"MVR":15.400167,"MWK":725.535,"MXN":18.804893,"MYR":4.222083,"MZN":61.115,"NAD":13.535527,"NGN":359.805566,"NIO":30.746369,"NOK":7.9722,"NPR":104.009834,"NZD":1.424587,"OMR":0.385015,"PAB":1,"PEN":3.236683,"PGK":3.201466,"PHP":51.406,"PKR":105.230492,"PLN":3.585395,"PYG":5639.3,"QAR":3.672571,"RON":3.883094,"RSD":100.656494,"RUB":57.54295,"RWF":854.182838,"SAR":3.7512,"SBD":7.777811,"SCR":13.768427,"SDG":6.675868,"SEK":8.160019,"SGD":1.35689,"SHP":0.758867,"SLL":7643.328121,"SOS":578.460064,"SRD":7.448,"SSP":130.2183,"STD":20769.150676,"SVC":8.748749,"SYP":514.98999,"SZL":13.533002,"THB":33.124,"TJS":8.799001,"TMT":3.499986,"TND":2.458496,"TOP":2.224732,"TRY":3.660214,"TTD":6.729148,"TWD":30.242233,"TZS":2244.6,"UAH":26.506511,"UGX":3656.55,"USD":1,"UYU":29.121233,"UZS":8057.7,"VEF":9.990025,"VND":22722.253308,"VUV":105.316668,"WST":2.533273,"XAF":554.275221,"XAG":0.05869414,"XAU":0.00077711,"XCD":2.70255,"XDR":0.70955,"XOF":554.275221,"XPD":0.0010424,"XPF":100.833786,"XPT":0.00108021,"YER":250.281642,"ZAR":13.539342,"ZMW":9.748924,"ZWL":322.355011}} \ No newline at end of file diff --git a/README.md b/README.md index 15ecd5245b..3c7a1c5803 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ New version of Topcoder Community website. ### Knowledgebase -- [Challenge Terms - Mocking for Testing and Development](docs/challenge-terms.md) +- [Mocking Terms for Testing and Development](docs/mocking-terms.md) - [Code Splitting](docs/code-splitting.md) - [Coding Standards](docs/coding-standards.md) - [How to Add a New Topcoder Community?](docs/how-to-add-a-new-topcoder-community.md) diff --git a/__tests__/shared/components/__snapshots__/Content.jsx.snap b/__tests__/shared/components/__snapshots__/Content.jsx.snap index 5afa0a1916..5373e90f7a 100644 --- a/__tests__/shared/components/__snapshots__/Content.jsx.snap +++ b/__tests__/shared/components/__snapshots__/Content.jsx.snap @@ -343,6 +343,22 @@ exports[`Matches shallow shapshot 1`] = ` +

+ Sandbox +

+

+ The right place to put any experimental and proof-of-concept stuff. +

+

Misc Examples

diff --git a/__tests__/shared/components/__snapshots__/Select.jsx.snap b/__tests__/shared/components/__snapshots__/Select.jsx.snap index ac2629da31..1c796a5012 100644 --- a/__tests__/shared/components/__snapshots__/Select.jsx.snap +++ b/__tests__/shared/components/__snapshots__/Select.jsx.snap @@ -21,7 +21,6 @@ exports[`Matches shallow shapshot 1`] = ` ignoreAccents={true} ignoreCase={true} inputProps={Object {}} - instanceId="id" isLoading={false} joinValues={false} labelKey="label" diff --git a/__tests__/shared/components/tc-communities/communities/community-2/__snapshots__/Home.jsx.snap b/__tests__/shared/components/tc-communities/communities/community-2/__snapshots__/Home.jsx.snap index 6694515b1d..6f14abbb9e 100644 --- a/__tests__/shared/components/tc-communities/communities/community-2/__snapshots__/Home.jsx.snap +++ b/__tests__/shared/components/tc-communities/communities/community-2/__snapshots__/Home.jsx.snap @@ -19,7 +19,7 @@ exports[`Match shadow snapshot 1`] = ` title="Community 2" /> - + - + { data: { challengeGroupId: '1', communityName: 'name', + communityId: '1', }, }, hideJoinButton: true, @@ -44,6 +46,9 @@ describe('full render connnected component and dispatch actions', () => { groups: { groups: {}, }, + terms: { + terms: [], + }, }; const mockStore = configureStore(); @@ -56,7 +61,7 @@ describe('full render connnected component and dispatch actions', () => { json: () => ({ result: { status: 200, metadata: {}, content: [] } }), }); store = mockStore(initialState); - instance = mount(); + instance = mount(); joinCommunity = instance.find(JoinCommunity); store.clearActions(); }); @@ -74,7 +79,7 @@ describe('full render connnected component and dispatch actions', () => { }, }; store = mockStore(newStore); - instance = mount(); + instance = mount(); }); test('hideJoinButton', () => { diff --git a/config/custom-environment-variables.json b/config/custom-environment-variables.json index 42e4c95eda..4218407c57 100644 --- a/config/custom-environment-variables.json +++ b/config/custom-environment-variables.json @@ -3,6 +3,6 @@ { "LOG_ENTRIES_TOKEN": "LOG_ENTRIES_TOKEN", - "MOCK_CHALLENGE_TERMS_SERVICE": "MOCK_CHALLENGE_TERMS_SERVICE" + "MOCK_TERMS_SERVICE": "MOCK_TERMS_SERVICE" } diff --git a/config/default.json b/config/default.json index 8e05b44709..639e54ce87 100644 --- a/config/default.json +++ b/config/default.json @@ -29,11 +29,11 @@ * the setup. To override it use LOG_ENTRIES_TOKEN environment variable. */ "LOG_ENTRIES_TOKEN": "816f5574-0d4a-49f9-ab3b-00d791f7c1f7", - /* When set to true, challenge terms service is short-cut so that each time - * a user goes to challenge details page, it tells that none of challenge + /* When set to true, terms service is short-cut so that each time + * a user goes to challenge details page or community page, it tells that none of * terms is agrees, and takes care that user is taken through the terms * agreement flow. */ - "MOCK_CHALLENGE_TERMS_SERVICE": false, + "MOCK_TERMS_SERVICE": false, /* Configuration related to https://openexchangerates.org. This is the * service which provides currency exchange rates. */ diff --git a/config/webpack/default.js b/config/webpack/default.js index 07c0e8aba1..2bdb77c519 100644 --- a/config/webpack/default.js +++ b/config/webpack/default.js @@ -28,7 +28,7 @@ module.exports = { ], loader: 'file-loader', options: { - outputPath: '/fonts/', + outputPath: '/community-app-fonts/', publicPath: '', }, }, { diff --git a/docs/challenge-terms.md b/docs/challenge-terms.md deleted file mode 100644 index 9142e35583..0000000000 --- a/docs/challenge-terms.md +++ /dev/null @@ -1,15 +0,0 @@ -# Challenge Terms - Mocking for Testing and Development - -Development and testing related to challenge terms is not straightforward because in dev. environment related functionality does not work perfect, and in prod. it is not possible to reset agreement for a terms without admin privileges. Thus, we have a mock of challenge terms service that helps to nail development, and also to write related unit-tests with Jest. - -### Development - -To enable challenge terms mocking for development, run the app with `MOCK_CHALLENGE_TERMS_SERVICE` environment variable set to `true`. E.g., on Linux, in dev. mode against production backend, you execute `$ MOCK_CHALLENGE_TERMS_SERVICE=true NODE_ENV=production PORT=80 npm run dev`. - -With this option enabled, each challenge you access will be protected by two terms: a simple Topcoder ones + mock DocuSign NDA (really simple mock, renders a small HTML page, that shows buttons that, when pressed, imitate DocuSign callbacks in the cases of terms agreement or rejection). By default, both terms won't be agreed initially. Agreeing with them won't be stored in the service mock (so that if you reload a challenge page, you'll see that both terms are pending to be agreed with again). - -The code of the mock module, and mock data for it, can be found in the `/src/shared/services/__mocks__` folder. You can modify them for your development and testing need, but, please, don't commit these changes to the repo, as we are planning to write unit-tests using this mock (if there is a really good reason to update the mock, be sure to check that related unit-tests are updated appropriately as well). - -### Unit-Testing - -Using this challenge terms mock for unit-testing with Jest is straightfoward: you add to an unit-test module `jest.mock('services/terms')`, and the module will be mocked using the same logic as for development. diff --git a/docs/how-to-add-a-new-topcoder-community.md b/docs/how-to-add-a-new-topcoder-community.md index f5051afbb6..3e609afd7c 100644 --- a/docs/how-to-add-a-new-topcoder-community.md +++ b/docs/how-to-add-a-new-topcoder-community.md @@ -52,7 +52,8 @@ To add a new community with the name **demo**, we should follow the following pr ], "newsFeed": "http://www.topcoder.com/feed", "description": "A berief description which will be displayed in dashboard", - "image": "1.jpg" + "image": "1.jpg", + "terms": [21153] } ``` Its fields serve the following purposes: @@ -116,6 +117,8 @@ To add a new community with the name **demo**, we should follow the following pr The `` component does not render anything, if its `news` property is *null* or an empty array, thus it can be kept inside the page code even when there is no news feed configured for a community. - `description`: A berief description which will be displayed in dashboard. - `image`: A image that located at `/assets/images/tc-communities/background` will be displayed in dashboard + - `terms` - *Array of Numbers* - Optional. If provided, it should hold an array of Topcoder term of use IDs; agreement to all these terms will be necessary to self-join the community. Beside this, it has no other effects at the moment. + 3. Custom pages of the community (anything beside `Challenges` and `Leaderboard`) should be created inside `/src/shared/components/tc-communities/communities/demo`. At the moment all communities have two custom pages: `Home` and `Learn`, you may just copy these from an existing community, and then customize to your particular needs. 4. The routing inside community, and code splitting of the related code, should be set up inside `/src/shared/routes/Communities`: diff --git a/docs/mocking-terms.md b/docs/mocking-terms.md new file mode 100644 index 0000000000..00edd23129 --- /dev/null +++ b/docs/mocking-terms.md @@ -0,0 +1,15 @@ +# Mocking Terms for Testing and Development + +Development and testing related to challenges and communities terms is not straightforward because in dev. environment related functionality does not work perfect, and in prod. it is not possible to reset agreement for a terms without admin privileges. Thus, we have a mock of terms service that helps to nail development, and also to write related unit-tests with Jest. + +### Development + +To enable terms mocking for development, run the app with `MOCK_TERMS_SERVICE` environment variable set to `true`. E.g., on Linux, in dev. mode against production backend, you execute `$ MOCK_TERMS_SERVICE=true NODE_ENV=production PORT=80 npm run dev`. Against development backend it would be simply `$ MOCK_TERMS_SERVICE=true npm run dev`. + +With this option enabled, each challenge and community you access will be protected by two terms if user is not authenticated: a simple Topcoder ones + mock DocuSign NDA (really simple mock, renders a small HTML page, that shows buttons that, when pressed, imitate DocuSign callbacks in the cases of terms agreement or rejection). If a user is authenticated then each challenge and community will be protected by three terms one of which is agreed. By default, two terms won't be agreed initially. Agreeing with them won't be stored in the service mock though after all terms agreed `checkStatusDone` action will return all terms agreed. If you reload a page, you'll see that both terms are pending to be agreed with again. + +The code of the mock module, and mock data for it, can be found in the `/src/shared/services/__mocks__` folder. You can modify them for your development and testing need, but, please, don't commit these changes to the repo, as we are planning to write unit-tests using this mock (if there is a really good reason to update the mock, be sure to check that related unit-tests are updated appropriately as well). + +### Unit-Testing + +Using this terms mock for unit-testing with Jest is straightforward: you add to an unit-test module `jest.mock('services/terms')`, and the module will be mocked using the same logic as for development. diff --git a/src/assets/images/logo-topcoder-mono.svg b/src/assets/images/logo-topcoder-mono.svg new file mode 100644 index 0000000000..1e6de3c6db --- /dev/null +++ b/src/assets/images/logo-topcoder-mono.svg @@ -0,0 +1 @@ +connect logo positive mono \ No newline at end of file diff --git a/src/assets/images/sandbox/payments/close.svg b/src/assets/images/sandbox/payments/close.svg new file mode 100644 index 0000000000..bb6b4544ab --- /dev/null +++ b/src/assets/images/sandbox/payments/close.svg @@ -0,0 +1,16 @@ + + + + Shape + Created with Sketch. + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/sandbox/payments/pattern-waves-gray.png b/src/assets/images/sandbox/payments/pattern-waves-gray.png new file mode 100755 index 0000000000..16c3ccb09f Binary files /dev/null and b/src/assets/images/sandbox/payments/pattern-waves-gray.png differ diff --git a/src/assets/images/sandbox/payments/pattern-waves-green.png b/src/assets/images/sandbox/payments/pattern-waves-green.png new file mode 100755 index 0000000000..84cf224763 Binary files /dev/null and b/src/assets/images/sandbox/payments/pattern-waves-green.png differ diff --git a/src/assets/images/sandbox/payments/status/active.svg b/src/assets/images/sandbox/payments/status/active.svg new file mode 100644 index 0000000000..a799078fde --- /dev/null +++ b/src/assets/images/sandbox/payments/status/active.svg @@ -0,0 +1,34 @@ + + + + status icon/active + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/sandbox/payments/status/canceled.svg b/src/assets/images/sandbox/payments/status/canceled.svg new file mode 100644 index 0000000000..cd0b670f17 --- /dev/null +++ b/src/assets/images/sandbox/payments/status/canceled.svg @@ -0,0 +1,14 @@ + + + + status icon/canceled + Created with Sketch. + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/sandbox/payments/status/completed.svg b/src/assets/images/sandbox/payments/status/completed.svg new file mode 100644 index 0000000000..206529dd29 --- /dev/null +++ b/src/assets/images/sandbox/payments/status/completed.svg @@ -0,0 +1,15 @@ + + + + status icon/completed + Created with Sketch. + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/images/sandbox/payments/status/draft.svg b/src/assets/images/sandbox/payments/status/draft.svg new file mode 100644 index 0000000000..2cc7896a0f --- /dev/null +++ b/src/assets/images/sandbox/payments/status/draft.svg @@ -0,0 +1,12 @@ + + + + status icon/draft + Created with Sketch. + + + + + + + \ No newline at end of file diff --git a/src/assets/images/sandbox/payments/status/paused.svg b/src/assets/images/sandbox/payments/status/paused.svg new file mode 100644 index 0000000000..e60863234b --- /dev/null +++ b/src/assets/images/sandbox/payments/status/paused.svg @@ -0,0 +1,20 @@ + + + + status icon/paused + Created with Sketch. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/client/index.jsx b/src/client/index.jsx index d12e7d1c3c..75e62e95b5 100644 --- a/src/client/index.jsx +++ b/src/client/index.jsx @@ -2,7 +2,8 @@ * Client-side rendering of the App. */ -import actions from 'actions/auth'; +import authActions from 'actions/auth'; +import directActions from 'actions/direct'; import userGroupsActions from 'actions/groups'; import cookies from 'browser-cookies'; import { BrowserRouter, browserHistory } from 'react-router-dom'; @@ -20,6 +21,12 @@ import logger from 'utils/logger'; import storeFactory from '../shared/store-factory'; import './styles.scss'; +const actions = { + ...authActions, + ...directActions, + ...userGroupsActions, +}; + /* Isomorphic code may rely on this environment variable to check whether it is * executed client- or server-side. */ if (!process.env.FRONT_END) { @@ -66,16 +73,22 @@ function authenticate(store) { store.dispatch(actions.auth.setTcTokenV2(tctV2)); } - /* User group data demands proper authorization to be accessed, - * thus they should be dropped if authentication fails. */ - if (!tctV3) { - store.dispatch(userGroupsActions.groups.dropGroups()); + const userV3 = tctV3 ? decodeToken(tctV3) : {}; + const prevUserV3 = auth.tokenV3 ? decodeToken(auth.tokenV3) : {}; + + /* If we enter the following "if" block, it means that our visitor used + * to be authenticated before, but now he has lost his authentication; + * or he has authenticated as a different user. In both cases, we must drop + * from the state all sensitive data, accessible only to specific users. */ + if (prevUserV3.handle && prevUserV3.handle !== userV3.handle) { + store.dispatch(actions.direct.dropAll()); + store.dispatch(actions.groups.dropGroups()); } /* Automatic refreshment of auth tokens. */ let time = Number.MAX_VALUE; if (tctV2) time = decodeToken(tctV2).exp; - if (tctV3) time = Math.min(time, decodeToken(tctV3).exp); + if (userV3) time = Math.min(time, userV3.exp); if (time < Number.MAX_VALUE) { time = 1000 * (time - config.REAUTH_TIME); time = Math.max(0, time - Date.now()); diff --git a/src/server/tc-communities/blockchain/metadata.json b/src/server/tc-communities/blockchain/metadata.json index 0116c7414a..a43bce004a 100644 --- a/src/server/tc-communities/blockchain/metadata.json +++ b/src/server/tc-communities/blockchain/metadata.json @@ -25,5 +25,5 @@ "leaderboardApiUrl": "https://api.topcoder.com/v4/looks/458/run/json/", "newsFeed": "http://www.topcoder.com/feed", "description": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet dolore.", - "image":"1.jpg" + "image": "1.jpg" } diff --git a/src/server/tc-communities/tc-prod-dev/metadata.json b/src/server/tc-communities/tc-prod-dev/metadata.json index 3dca42a275..e03e548a4e 100644 --- a/src/server/tc-communities/tc-prod-dev/metadata.json +++ b/src/server/tc-communities/tc-prod-dev/metadata.json @@ -1,6 +1,6 @@ { "challengeFilter": { - "groupIds": ["20000001"] + "groupIds": ["20000013"] }, "communityId": "tc-prod-dev", "communityName": "Topcoder Product Development", @@ -16,7 +16,8 @@ "redirect": "https://ios.topcoder.com/", "value": "3" }], - "groupIds": ["20000001"], + "groupIds": ["20000013"], + "terms": [21193,21153], "logos": [{ "img": "/themes/tc-prod-dev/logo_topcoder_with_name.svg", "url": "https://www.topcoder.com" diff --git a/src/shared/actions/direct.js b/src/shared/actions/direct.js new file mode 100644 index 0000000000..f494c84903 --- /dev/null +++ b/src/shared/actions/direct.js @@ -0,0 +1,67 @@ +/** + * Direct-related actions: projects, billing accounts, copilot, and other + * similar stuff should be handled here, at least for now. + */ + +import { createActions } from 'redux-actions'; +import { getService } from 'services/direct'; + +/** + * Payload creator (noop in fact) for the action that drops all Direct-related + * data out of the Redux state; and cancels any pending loading requests. + */ +function dropAll() { + return null; +} + +/** + * Payload creator for the action that inits loading of the specified project + * details. + * @param {Number} projectId + * @return {Number} + */ +function getProjectDetailsInit(projectId) { + return projectId; +} + +/** + * Payload creator for the action that actually loads the details of + * the specified project. + * @param {Number} projectId + * @param {String} tokenV3 Topcoder auth token v3. + * @return {Promise} Resolves to the project details object. + */ +function getProjectDetailsDone(projectId, tokenV3) { + return getService(tokenV3).getProjectDetails(projectId); +} + +/** + * Payload creator for the action that inits the loading of projects related to + * the user. + * @param {String} tokenV3 Topcoder auth token v3. + * @return {String} Topcoder auth token v3. + */ +function getUserProjectsInit(tokenV3) { + return tokenV3; +} + +/** + * Payload creator for the actio that actually pulls from API the projects + * related to user. + * @param {String} tokenV3 Topcoder auth token v3. + * @return {Object} Pull result object + some meta-information. + */ +async function getUserProjectsDone(tokenV3) { + const projects = await getService(tokenV3).getUserProjects(); + return { tokenV3, projects }; +} + +export default createActions({ + DIRECT: { + DROP_ALL: dropAll, + GET_PROJECT_DETAILS_INIT: getProjectDetailsInit, + GET_PROJECT_DETAILS_DONE: getProjectDetailsDone, + GET_USER_PROJECTS_INIT: getUserProjectsInit, + GET_USER_PROJECTS_DONE: getUserProjectsDone, + }, +}); diff --git a/src/shared/actions/index.js b/src/shared/actions/index.js new file mode 100644 index 0000000000..26f3bfcea8 --- /dev/null +++ b/src/shared/actions/index.js @@ -0,0 +1,17 @@ +/** + * This module combines all available actions into a single object, which is + * more convenient for use inside containers that need access to different + * groups of actions. + */ + +import challengeActions from './challenge'; +import directActions from './direct'; +import memberTasks from './member-tasks'; +import pageActions from './page'; + +export default { + ...challengeActions, + ...directActions, + ...memberTasks, + ...pageActions, +}; diff --git a/src/shared/actions/member-tasks.js b/src/shared/actions/member-tasks.js new file mode 100644 index 0000000000..66365904e2 --- /dev/null +++ b/src/shared/actions/member-tasks.js @@ -0,0 +1,76 @@ +/** + * Actions for management of member tasks and payments. Under the hood it is + * very similar to the challenge listing management, as these tasks are in fact + * just challenges of a special kind); however, due to differences in the use + * cases, we can implement task management more efficient with dedicated actions + * and reducer; thus, this module. + */ + +import { createActions } from 'redux-actions'; +import { getService } from 'services/challenges'; + +/** + * Holds the size of member tasks page for the MEMBER_TASK/GET_DONE action. + * Mind that current version of TC API v3 restricts the possible page size + * by 50 tasks anyway, thus setting this to a larger value will result in + * problems. + */ +export const PAGE_SIZE = 50; + +/** + * Payload creator for the action that drops all loaded member tasks from Redux + * state, and cancels any pending GET_DONE actions. + * This no operation function is here just for the sake of documentation. + */ +function dropAll() { + return null; +} + +/** + * Payload creator for the action that inits the loading of a member tasks page. + * Note that dispatching this action before a previous loading operation has + * been completed will cancel the previous loading operation. + * @param {String} uuid UUID of the loading operation. + * @param {Number} pageNum 0-based index of the page to load (PAGE_SIZE constant + * holds the page size). + * @return {Object} Action payload. + */ +function getInit(uuid, pageNum) { + return { pageNum, uuid }; +} + +/** + * Payload creator for the action that actually loads a page of member tasks. + * Prior to this action always dispatch the MEMBER_TASKS/GET_INIT action with + * the same arguments. The result of MEMBER_TASKS/GET_DONE will be silently + * discarted if its uuid is not stored in the Redux list of pending requests + * to load tasks. + * @param {String} uuid UUID of the loading operation. + * @param {String} projectId Project filter for tasks. + * @param {Number} pageNum 0-based index of the page to load (PAGE_SIZE constant + * holds the page size). + * @param {String} tokenV3 Topcoder v3 auth token. + * @return {Object} Action payload. + */ +function getDone(uuid, projectId, pageNum, tokenV3) { + return getService(tokenV3).getChallenges({ + isTask: 1, + projectId, + }, { + limit: PAGE_SIZE, + offset: pageNum * PAGE_SIZE, + }).then(({ challenges, totalCount }) => ({ + projectId, + tasks: challenges, + totalCount, + uuid, + })); +} + +export default createActions({ + MEMBER_TASKS: { + DROP_ALL: dropAll, + GET_INIT: getInit, + GET_DONE: getDone, + }, +}); diff --git a/src/shared/actions/page/index.js b/src/shared/actions/page/index.js new file mode 100644 index 0000000000..db1afa4af2 --- /dev/null +++ b/src/shared/actions/page/index.js @@ -0,0 +1,3 @@ +import sandboxActions from './sandbox'; + +export default sandboxActions; diff --git a/src/shared/actions/page/sandbox/index.js b/src/shared/actions/page/sandbox/index.js new file mode 100644 index 0000000000..fcc2d6c11b --- /dev/null +++ b/src/shared/actions/page/sandbox/index.js @@ -0,0 +1,3 @@ +import paymentActions from './payments'; + +export default paymentActions; diff --git a/src/shared/actions/page/sandbox/payments/editor.js b/src/shared/actions/page/sandbox/payments/editor.js new file mode 100644 index 0000000000..2555708835 --- /dev/null +++ b/src/shared/actions/page/sandbox/payments/editor.js @@ -0,0 +1,94 @@ +/** + * Actions related to the UI state of payment editor page. + */ + +import { createActions } from 'redux-actions'; + +export const STATE = { + NEW_PAYMENT: 'NEW_PAYMENT', + PAID: 'PAID', + WAITING_PAYMENT_ACTIVATION: 'WAITING_PAYMENT_ACTIVATION', + WAITING_PAYMENT_CLOSURE: 'WAITING_PAYMENT_CLOSURE', + WAITING_PAYMENT_DRAFT: 'WAITING_PAYMENT_DRAFT', +}; + +/** + * Payload creator for the action that selects the specified billing account. + * @param {Number} accountId + * @return {Number} Payload. + */ +function selectBillingAccount(accountId) { + return accountId; +} + +/** + * Payload creator for the action that selects the specified project. + * @param {String} projectId + * @return {String} Action payload. + */ +function selectProject(projectId) { + return projectId; +} + +/** + * Payload creator for the action that switches page states. See STATE enum. + * @param {String} state + * @return {String} + */ +function setPageState(state) { + return state; +} + +/** + * Payload creator for the action that sets the payment amount. + * @param {Number} amount Payment amount in USD. + * @return {Number} + */ +function setPaymentAmount(amount) { + return amount; +} + +/** + * Payload creator for the action that sets the payment assignee. + * @param {String} username + * @return {String} + */ +function setPaymentAssignee(username) { + return username; +} + +/** + * Payload creator for the action that sets the payment description. + * @param {String} description + * @return {String} Action payload. + */ +function setPaymentDescription(description) { + return description; +} + +/** + * Payload creator for the action that sets the payment title. + * @param {String} title + * @return {String} Action payload. + */ +function setPaymentTitle(title) { + return title; +} + +export default createActions({ + PAGE: { + SANDBOX: { + PAYMENTS: { + EDITOR: { + SELECT_BILLING_ACCOUNT: selectBillingAccount, + SELECT_PROJECT: selectProject, + SET_PAGE_STATE: setPageState, + SET_PAYMENT_AMOUNT: setPaymentAmount, + SET_PAYMENT_ASSIGNEE: setPaymentAssignee, + SET_PAYMENT_DESCRIPTION: setPaymentDescription, + SET_PAYMENT_TITLE: setPaymentTitle, + }, + }, + }, + }, +}); diff --git a/src/shared/actions/page/sandbox/payments/index.js b/src/shared/actions/page/sandbox/payments/index.js new file mode 100644 index 0000000000..459b44d38b --- /dev/null +++ b/src/shared/actions/page/sandbox/payments/index.js @@ -0,0 +1,5 @@ +import _ from 'lodash'; +import editorActions from './editor'; +import listingActions from './listing'; + +export default _.merge(editorActions, listingActions); diff --git a/src/shared/actions/page/sandbox/payments/listing.js b/src/shared/actions/page/sandbox/payments/listing.js new file mode 100644 index 0000000000..4c3201b0f4 --- /dev/null +++ b/src/shared/actions/page/sandbox/payments/listing.js @@ -0,0 +1,26 @@ +/** + * Actions related to the UI state of member payments listing page. + */ + +import { createActions } from 'redux-actions'; + +/** + * Payload creator for the action that selects the specified project. + * @param {Number} projectId + * @return {String} Action payload. + */ +function selectProject(projectId) { + return projectId; +} + +export default createActions({ + PAGE: { + SANDBOX: { + PAYMENTS: { + LISTING: { + SELECT_PROJECT: selectProject, + }, + }, + }, + }, +}); diff --git a/src/shared/actions/terms.js b/src/shared/actions/terms.js index 8c173c2423..dad9ca9be2 100644 --- a/src/shared/actions/terms.js +++ b/src/shared/actions/terms.js @@ -3,31 +3,47 @@ */ import _ from 'lodash'; +import config from 'utils/config'; import { createActions } from 'redux-actions'; import { getService } from 'services/terms'; -/** - * Payload creator for TERMS/GET_TERMS_INIT action, - * which marks that we are about to fetch terms of the specified challenge. - * If any challenge terms for another challenge are currently being fetched, - * they will be silently discarded. - * @param {Number|String} challengeId - * @return {String} - */ -function getTermsInit(challengeId) { - return _.toString(challengeId); -} - /** * Payload creator for TERMS/GET_TERMS_DONE action, - * which fetch terms of the specified challenge. - * @param {Number|String} challengeId - * @param {String} tokenV2 + * which fetch terms of the specified entity. + * @param {Object} entity entity object + * @param {String} entity.type entity type ['challenge'||'community'] + * @param {String} entity.id entity id + * @param {Object} tokens object with tokenV2 and tokenV3 properties + * @param {Boolean} mockAgreed if true, then all terms will be mocked as agreed + * this only makes effect if MOCK_TERMS_SERVICE is true + * and the only purpose of this param is testing terms * @return {Promise} */ -function getTermsDone(challengeId, tokenV2) { - const service = getService(tokenV2); - return service.getTerms(challengeId).then(res => ({ challengeId, terms: res.terms })); +function getTermsDone(entity, tokens, mockAgreed) { + const service = getService(tokens.tokenV2); + let termsPromise; + + // if mockAgreed=true passed, then we create an array of 10 true which we pass to the + // terms service methods. + // when terms service is mocked by setting MOCK_TERMS_SERVICE=true + // it will make all terms to have agreed status (actually only first 10 will be agreed, + // but we will hardly have even more then 3 terms per entity) + const mockAgreedArray = mockAgreed ? Array(10 + 1).join('1').split('').map(() => true) : []; + + switch (entity.type) { + case 'challenge': { + termsPromise = service.getChallengeTerms(entity.id, mockAgreedArray); + break; + } + case 'community': { + termsPromise = service.getCommunityTerms(entity.id, tokens.tokenV3, mockAgreedArray); + break; + } + default: + throw new Error(`Entity type '${entity.type}' is not supported by getTermsDone.`); + } + + return termsPromise.then(res => ({ entity, terms: res.terms })); } /** @@ -99,7 +115,7 @@ function agreeTermDone(termId, tokenV2) { /** * Payload creator for TERMS/CHECK_STATUS_DONE - * which will check if all terms of specified challenge have been agreed, + * which will check if all terms of specified entity have been agreed, * * NOTE: * As in some reason backend does not saves immediately that DocuSign term has been agreed @@ -107,19 +123,35 @@ function agreeTermDone(termId, tokenV2) { * Maximum quantity attempts and delay between attempts are configured in * MAX_ATTEMPTS and TIME_OUT * - * @param {Number|String} challengeId id of challenge to check - * @param {String} tokenV2 auth token + * TODO: + * Looks like the bug described above was caused by server caching responses + * at least for getTermDetails which is used by getCommunityTerms. + * To fix it I've added nocache random value param in the terms service + * for getTermDetails and it looks like works so we get results immediately. + * This still have to be tested for challenges as they use another endpoint + * in method getChallengeTerms. + * Also terms which use third part service DocuSign has to be also tested prior + * to removing multiple checks. + * In case their agreed status is updated immediately, this code + * has to simplified and don't make several attempts, only one. + * + * @param {Object} entity entity object + * @param {String} entity.type entity type ['challenge'||'community'] + * @param {String} entity.id entity id + * @param {Object} tokens object with tokenV2 and tokenV3 properties * * @return {Promise} promise of request result */ -function checkStatusDone(challengeId, tokenV2) { +function checkStatusDone(entity, tokens) { // timeout between checking status attempts const TIME_OUT = 5000; // maximum attempts to check status const MAX_ATTEMPTS = 5; - const service = getService(tokenV2); + // we set this flag for getTermsDone when MOCK_TERMS_SERVICE is true + // so that checkStatusDone resolves to all terms agreed when mocking + const mockAgreed = config.MOCK_TERMS_SERVICE; /** * Promisified setTimeout @@ -137,7 +169,7 @@ function checkStatusDone(challengeId, tokenV2) { * @param {Number} maxAttempts maximum number of attempts to perform * @return {Promise} resolves to the list of term objects */ - const checkStatus = maxAttempts => service.getTerms(challengeId).then((res) => { + const checkStatus = maxAttempts => getTermsDone(entity, tokens, mockAgreed).then((res) => { const allAgreed = _.every(res.terms, 'agreed'); // if not all terms are agreed and we still have some attempts to try @@ -153,7 +185,7 @@ function checkStatusDone(challengeId, tokenV2) { export default createActions({ TERMS: { - GET_TERMS_INIT: getTermsInit, + GET_TERMS_INIT: _.identity, GET_TERMS_DONE: getTermsDone, GET_TERM_DETAILS_INIT: getTermDetailsInit, GET_TERM_DETAILS_DONE: getTermDetailsDone, diff --git a/src/shared/components/Content/index.jsx b/src/shared/components/Content/index.jsx index 3d0b162693..20e1108a60 100644 --- a/src/shared/components/Content/index.jsx +++ b/src/shared/components/Content/index.jsx @@ -138,6 +138,17 @@ export default function Content() { +

Sandbox

+

+ The right place to put any experimental and proof-of-concept + stuff. +

+
    +
  • + Payments PoC +
  • +
+

Misc Examples

  • diff --git a/src/shared/components/Select/index.jsx b/src/shared/components/Select/index.jsx index bfdae162e5..67e85bb3d8 100644 --- a/src/shared/components/Select/index.jsx +++ b/src/shared/components/Select/index.jsx @@ -1,5 +1,4 @@ import React from 'react'; -import PT from 'prop-types'; import ReactSelect from 'react-select'; import 'react-select/dist/react-select.css'; @@ -12,15 +11,6 @@ export default function Select(props) { {...props} autosize={false} className={style.select} - instanceId={props.id} /> ); } - -Select.defaultProps = { - -}; - -Select.propTypes = { - id: PT.string.isRequired, -}; diff --git a/src/shared/components/challenge-detail/ChallengeTerms/TermDetails.jsx b/src/shared/components/Terms/TermDetails.jsx similarity index 96% rename from src/shared/components/challenge-detail/ChallengeTerms/TermDetails.jsx rename to src/shared/components/Terms/TermDetails.jsx index 301e0ffb70..27e5c372df 100644 --- a/src/shared/components/challenge-detail/ChallengeTerms/TermDetails.jsx +++ b/src/shared/components/Terms/TermDetails.jsx @@ -1,3 +1,6 @@ +/** + * Terms details component which display text of an agreement + */ /* eslint react/no-danger:0 */ import React from 'react'; diff --git a/src/shared/components/challenge-detail/ChallengeTerms/TermDetails.scss b/src/shared/components/Terms/TermDetails.scss similarity index 100% rename from src/shared/components/challenge-detail/ChallengeTerms/TermDetails.scss rename to src/shared/components/Terms/TermDetails.scss diff --git a/src/shared/components/challenge-detail/ChallengeTerms/index.jsx b/src/shared/components/Terms/index.jsx similarity index 96% rename from src/shared/components/challenge-detail/ChallengeTerms/index.jsx rename to src/shared/components/Terms/index.jsx index 7b2a2978bf..708b44f796 100644 --- a/src/shared/components/challenge-detail/ChallengeTerms/index.jsx +++ b/src/shared/components/Terms/index.jsx @@ -1,3 +1,6 @@ +/** + * Terms component which displays modal window with term details + */ /* eslint jsx-a11y/no-static-element-interactions:0 */ /* global window */ @@ -43,7 +46,7 @@ function handleScroll(scrollElement, masks, orientation) { } } -export default class ChallengeTerms extends React.Component { +export default class Terms extends React.Component { constructor(props) { super(props); @@ -123,7 +126,8 @@ export default class ChallengeTerms extends React.Component { render() { const { onCancel, terms, details, loadingTermId, docuSignUrl, getDocuSignUrl, agreeTerm, agreeingTerm, isLoadingTerms, - loadingDocuSignUrl, selectedTerm, viewOnly, checkingStatus } = this.props; + loadingDocuSignUrl, selectedTerm, viewOnly, checkingStatus, + description } = this.props; const handleHorizonalScroll = (e) => { const scrollElement = e.target; @@ -164,8 +168,7 @@ export default class ChallengeTerms extends React.Component { >
    -
    You are seeing these Terms & Conditions because you have registered to a challenge and - you have to respect the terms below in order to be able to submit.
    +
    {description}
    { checkingStatus && @@ -286,21 +289,21 @@ export default class ChallengeTerms extends React.Component { } } -ChallengeTerms.defaultProps = { +Terms.defaultProps = { terms: [], - title: '', + description: '', details: {}, loadingTermId: '', docuSignUrl: '', agreeingTerm: '', isLoadingTerms: false, - registering: false, loadingDocuSignUrl: '', selectedTerm: null, viewOnly: false, }; -ChallengeTerms.propTypes = { +Terms.propTypes = { + description: PT.string, onCancel: PT.func.isRequired, terms: PT.arrayOf(PT.shape()), loadDetails: PT.func.isRequired, diff --git a/src/shared/components/challenge-detail/ChallengeTerms/styles.scss b/src/shared/components/Terms/styles.scss similarity index 99% rename from src/shared/components/challenge-detail/ChallengeTerms/styles.scss rename to src/shared/components/Terms/styles.scss index 4a9c192ddf..605bcafd8f 100644 --- a/src/shared/components/challenge-detail/ChallengeTerms/styles.scss +++ b/src/shared/components/Terms/styles.scss @@ -11,6 +11,7 @@ } .container { + color: #000; font-family: roboto; } diff --git a/src/shared/components/sandbox/payments/Background/index.jsx b/src/shared/components/sandbox/payments/Background/index.jsx new file mode 100644 index 0000000000..e48939df51 --- /dev/null +++ b/src/shared/components/sandbox/payments/Background/index.jsx @@ -0,0 +1,43 @@ +/** + * The common background for payment-related pages. It has grayish pattern, + * black Topcoder logo in the top-left corner, and optional escape button + * in the top-right corner. + */ + +import PT from 'prop-types'; +import React from 'react'; +import TopcoderLogo from '../../../../../assets/images/logo-topcoder-mono.svg'; +import CloseIcon from '../../../../../assets/images/sandbox/payments/close.svg'; +import './style.scss'; + +/* TODO: For now it is just a placeholder that does nothing. */ +export default function Background({ children, onExit, escapeButton }) { + return ( +
    + + {escapeButton && ( + /* TODO: This should be updated to use our standard button + * compatible with react-redux routing. */ + +
    + +
    +
    ESC
    +
    + )} + {children} +
    + ); +} + +Background.defaultProps = { + children: null, + escapeButton: false, + onExit: () => {}, +}; + +Background.propTypes = { + children: PT.node, + onExit: PT.func, + escapeButton: PT.bool, +}; diff --git a/src/shared/components/sandbox/payments/Background/style.scss b/src/shared/components/sandbox/payments/Background/style.scss new file mode 100644 index 0000000000..f73c76a651 --- /dev/null +++ b/src/shared/components/sandbox/payments/Background/style.scss @@ -0,0 +1,37 @@ +@import "~styles/tc-styles"; + +.background { + background: url('assets/images/sandbox/payments/pattern-waves-gray.png'); + height: 100vh; + width: 100vw; +} + +.logo { + position: fixed; + left: 24px; + top: 24px; +} + +.esc { + position: fixed; + display: block; + right: 20px; + top: 20px; + cursor: pointer; + + .button { + box-sizing: border-box; + width: 40px; + height: 40px; + padding: 13px; + border-radius: 50%; + background: $tc-gray-10; + } + + .text { + margin-top: 20px; + font-size: 13px; + text-align: center; + color: $tc-gray-70; + } +} diff --git a/src/shared/components/sandbox/payments/Confirmation/index.jsx b/src/shared/components/sandbox/payments/Confirmation/index.jsx new file mode 100644 index 0000000000..c0253a2d1b --- /dev/null +++ b/src/shared/components/sandbox/payments/Confirmation/index.jsx @@ -0,0 +1,50 @@ +/** + * Payment confirmation. + */ + +import PT from 'prop-types'; +import React from 'react'; +import { Button, PrimaryButton } from 'components/buttons'; +import Background from '../Background'; + +import './style.scss'; + +export default function Confirmation({ + amount, + assignee, + resetPaymentData, +}) { + return ( + +
    +
    +

    Payment Completed

    +

    + Your payment has been accepted and money will be shortly transfered from + your account. +

    +
    +
    +

    ${amount} paid to {assignee}

    +

    Develop a new project submit button logic for the main page.

    +
    +
    +
    + + Ok, done for now +
    +
    +
    +
    + ); +} + +Confirmation.propTypes = { + amount: PT.number.isRequired, + assignee: PT.string.isRequired, + resetPaymentData: PT.func.isRequired, +}; diff --git a/src/shared/components/sandbox/payments/Confirmation/style.scss b/src/shared/components/sandbox/payments/Confirmation/style.scss new file mode 100644 index 0000000000..6c8aa27e4f --- /dev/null +++ b/src/shared/components/sandbox/payments/Confirmation/style.scss @@ -0,0 +1,89 @@ +@import "~styles/tc-styles"; + +.container { + display: block; + margin: auto; + padding-top: 50px; + width: 80%; + font-family: Roboto, sans-serif; +} + +.title { + font-weight: 300; + font-size: 36px; + color: $tc-black; + line-height: 40px; + text-align: center; +} + +.card { + box-sizing: border-box; + padding: 50px; + background: #fff; + box-shadow: 0 2px 9px 0 rgba(38, 38, 40, 0.06); + border-radius: 7px; + margin: 120px auto; + overflow: hidden; + height: 450px; + max-width: 540px; +} + +.description { + max-width: 440px; + margin: 30px auto; + font-size: 15px; + color: $tc-gray-80; + line-height: 25px; +} + +.paycheck { + background-color: $tc-green-10; + background-image: url('assets/images/sandbox/payments/pattern-waves-green.png'); + margin-top: 30px; + padding: 15px; + border: 1px solid $tc-green; + border-radius: 10px; + + .info { + box-sizing: border-box; + background: rgba(255, 255, 255, 0.9); + border-radius: 6px; + height: 100px; + padding: 15px; + + .user { + text-align: center; + font-size: 20px; + color: $tc-gray-50; + font-weight: 300; + line-height: 30px; + + > strong { + color: #202a36; + font-weight: 700; + } + + .name { + display: inline-block; + max-width: 200px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: top; + } + } + + .task { + margin-top: 15px; + text-align: center; + font-size: 13px; + color: $tc-black; + line-height: 20px; + } + } +} + +.actions { + margin-top: 15px; + text-align: center; +} diff --git a/src/shared/components/sandbox/payments/Editor/index.jsx b/src/shared/components/sandbox/payments/Editor/index.jsx new file mode 100644 index 0000000000..2321cc8f27 --- /dev/null +++ b/src/shared/components/sandbox/payments/Editor/index.jsx @@ -0,0 +1,172 @@ +/** + * Payment editor. + */ +/* global window */ + +import _ from 'lodash'; +import LoadingIndicator from 'components/LoadingIndicator'; +import PT from 'prop-types'; +import React from 'react'; +import Select from 'components/Select'; +import { PrimaryButton } from 'components/buttons'; + +import Avatar from '../../../Avatar'; +import Background from '../Background'; + +import './style.scss'; + +export default function Editor({ + makePayment, + neu, + paymentAmount, + paymentAssignee, + paymentDescription, + paymentTitle, + projectDetails, + projects, + selectedBillingAccountId, + selectedProjectId, + selectProject, + setPaymentAmount, + setPaymentAssignee, + setPaymentDescription, + setPaymentTitle, +}) { + let content; + if (!projectDetails) content = ; + else { + const billingAccounts = _.get(projectDetails, 'billingAccountIds', []) + .map(id => ({ label: id, value: id })); + content = billingAccounts.length ? ( +
    +
    + Billing account + setPaymentTitle(e.target.value)} + placeholder="Topcoder payment" + value={paymentTitle} + /> +
    +
    + Description +