├── .editorconfig
├── .gitignore
├── ISSUE_TEMPLATE.md
├── LICENSE.md
├── README.md
├── package.json
├── public
├── favicon.png
├── index.html
├── oauth-complete.html
├── site.css
└── user-menu-arrow.svg
└── src
├── components
├── activity
│ └── index.js
├── admin
│ ├── edit_task.js
│ ├── index.js
│ └── show_task.js
├── app
│ ├── app_header.js
│ ├── app_sidebar.js
│ └── index.js
├── shared
│ ├── create_task_modal.js
│ ├── error_modal.js
│ ├── login.js
│ ├── manage_users_modal.js
│ ├── settings_modal.js
│ └── success_modal.js
├── stats
│ ├── index.js
│ ├── stats_graph.js
│ ├── stats_header.js
│ └── stats_summary.js
└── task
│ ├── controls
│ └── task.js
│ ├── edit_bar.js
│ └── index.js
├── config
├── config_development.js
├── config_production.js
└── index.js
├── constants
├── activity_constants.js
├── admin_constants.js
├── items_constants.js
├── loading_constants.js
├── modals_constants.js
├── settings_constants.js
├── stats_constants.js
├── tasks_constants.js
└── user_constants.js
├── index.js
├── routes.js
├── services
├── index.js
└── server.js
├── stores
├── activity_action_creators.js
├── activity_reducer.js
├── activity_selectors.js
├── admin_action_creators.js
├── admin_reducer.js
├── admin_selectors.js
├── async_action.js
├── items_action_creators.js
├── items_reducer.js
├── items_selectors.js
├── loading_action_creators.js
├── loading_reducer.js
├── loading_selectors.js
├── modals_action_creators.js
├── modals_reducer.js
├── modals_selectors.js
├── schemas.js
├── settings_action_creators.js
├── settings_reducer.js
├── settings_selectors.js
├── stats_action_creators.js
├── stats_reducer.js
├── stats_selectors.js
├── store.js
├── tasks_action_creators.js
├── tasks_reducer.js
├── tasks_selectors.js
├── user_action_creators.js
├── user_reducer.js
└── user_selectors.js
└── utils
├── d3_graph.js
└── safe_storage.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.js]
2 | indent_style = space
3 | indent_size = 2
4 | trim_trailing_whitespace = true
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | build
4 | .DS_Store
5 | .env
6 | npm-debug.log
7 |
--------------------------------------------------------------------------------
/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ## I'm submitting a _(bug report/feature request/ support or question)_
12 |
13 | ## *Brief Description*
14 |
15 |
16 | ## What is the motivation / use case for this feature?
17 |
18 |
19 | ## What is the current behaviour, (attach relevant screenshots) ?
20 |
21 |
22 | ## What is the expected behaviour ?
23 |
24 |
25 | ## When does this occur ?
26 |
27 |
28 | ## How do we replicate the issue ?
29 |
30 | 1.
31 | 2.
32 | 3.
33 | 4.
34 |
35 | ## Please tell us about your environment:
36 |
37 |
38 | ## Other Information / context:
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2015, Aaron Lidman
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without
5 | modification, are permitted provided that the following conditions are met:
6 |
7 | * Redistributions of source code must retain the above copyright notice, this
8 | list of conditions and the following disclaimer.
9 |
10 | * Redistributions in binary form must reproduce the above copyright notice,
11 | this list of conditions and the following disclaimer in the documentation
12 | and/or other materials provided with the distribution.
13 |
14 | * Neither the name of [project] nor the names of its
15 | contributors may be used to endorse or promote products derived from
16 | this software without specific prior written permission.
17 |
18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ```
2 | ____ __ ____ __ _ _
3 | (_ _)/ \ ___( __)( )( \/ )
4 | )( ( O )(___)) _) )( ) (
5 | (__) \__/ (__) (__)(_/\_)
6 | ```
7 |
8 | A task manager for OpenStreetMap.
9 |
10 | Coordinate with other users and work down a queue of tasks without getting in each other’s way.
11 |
12 | 
13 |
14 | ## Related tools
15 |
16 | - A JOSM plugin is available at [tofix-plugin](https://github.com/JOSM/tofix-plugin).
17 | - A CLI for creating and updating tasks is available at [tofix-cli](https://github.com/Rub21/tofix-cli).
18 |
19 | ## Workflow
20 |
21 | This is the frontend component to `to-fix`. The server
22 | component is at [to-fix-backend](https://github.com/osmlab/to-fix-backend).
23 |
24 | This is a React + Redux application bootstrapped with [create-react-app](https://github.com/facebookincubator/create-react-app).
25 |
26 | To start development, you will require node.js to be installed.
27 |
28 | ```sh
29 | npm install && npm start
30 | ```
31 |
32 | To build and publish to `gh-pages`, run
33 |
34 | ```
35 | npm deploy
36 | ```
37 |
38 | You can configure some details at [src/config](src/config).
39 |
40 |
41 | ## Wiki
42 |
43 | * [How to create a new to-fix task](https://github.com/osmlab/to-fix/wiki/Creating-and-updating-tasks)
44 | * [How to work on an existing to-fix tasks](https://github.com/osmlab/to-fix/wiki/Working-on-a-task)
45 | * [List of open tasks](https://github.com/osmlab/to-fix/wiki/List-of-open-tasks)
46 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "TO-FIX",
3 | "version": "1.0.0",
4 | "description": "A task manager for OpenStreetMap",
5 | "private": true,
6 | "dependencies": {
7 | "@turf/bbox": "^3.7.5",
8 | "@turf/bbox-polygon": "^3.7.5",
9 | "@turf/centroid": "^3.5.3",
10 | "@turf/helpers": "^3.5.3",
11 | "d3": "^3.5.17",
12 | "file-size": "^1.0.0",
13 | "keymirror": "^0.1.1",
14 | "lodash.omit": "^4.5.0",
15 | "lodash.pick": "^4.4.0",
16 | "lodash.union": "^4.6.0",
17 | "mapbox-gl": "^0.32.1",
18 | "normalizr": "^2.2.1",
19 | "react": "^15.3.2",
20 | "react-dom": "^15.3.2",
21 | "react-keybinding": "^3.0.0",
22 | "react-redux": "^4.4.5",
23 | "react-router": "^3.0.0",
24 | "redux": "^3.6.0",
25 | "redux-logger": "^2.7.4",
26 | "redux-thunk": "^2.1.0",
27 | "reselect": "^2.5.4",
28 | "whatwg-fetch": "^1.0.0"
29 | },
30 | "devDependencies": {
31 | "gh-pages": "^0.11.0",
32 | "react-scripts": "0.8.1"
33 | },
34 | "scripts": {
35 | "start": "HTTPS=true react-scripts start",
36 | "build": "react-scripts build",
37 | "test": "react-scripts test --env=jsdom",
38 | "deploy": "npm run build && gh-pages -d build"
39 | },
40 | "repository": {
41 | "type": "git",
42 | "url": "https://github.com/osmlab/to-fix.git"
43 | },
44 | "author": "Aaron Lidman",
45 | "license": "BSD",
46 | "bugs": {
47 | "url": "https://github.com/osmlab/to-fix/issues"
48 | },
49 | "homepage": "https://osmlab.github.io/to-fix"
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/osmlab/to-fix/0ed4e7c32983eab62456e82493c883cad6be366b/public/favicon.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | TO-FIX
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/public/oauth-complete.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/public/site.css:
--------------------------------------------------------------------------------
1 | /* Base overrides
2 | ------------------------- */
3 | body {
4 | font:13px/20px 'Helvetica Neue', Helvetica, sans-serif;
5 | color:rgba(0,0,0,0.75);
6 | }
7 | h1, h2, h3, h4, h5, h6,
8 | strong,
9 | svg,
10 | button,
11 | .strong,
12 | .tabs a,
13 | input[type],
14 | label,
15 | form fieldset label,
16 | .button {
17 | font-family:'Helvetica Neue', Helvetica, sans-serif;
18 | }
19 | .strong { font-weight:500; }
20 | strong { font-weight:bold}
21 | .tabs a,
22 | input[type='submit'],
23 | button,
24 | .button {
25 | font-weight:bold;
26 | font-size:13px;
27 | line-height:18px;
28 | }
29 | h1, h2, h3 { font-weight:500; }
30 | h4, h5, h6 {
31 | font-size:15px;
32 | line-height:28px;
33 | }
34 | h2 { padding-top:0; padding-bottom:0; }
35 | .editbar .tabs > * {
36 | border-width:0 0 0 1px;
37 | background-color:rgba(0,0,0,0.75);
38 | color:#fff;
39 | border-radius:0;
40 | }
41 | .editbar .tabs > *:first-child {
42 | border-width:0;
43 | border-radius:3px 0 0 0;
44 | }
45 | .editbar .tabs > *:last-child {
46 | border-radius:0 3px 0 0;
47 | }
48 | .editbar .tabs > *:only-child {
49 | border-radius:3px 3px 0 0;
50 | }
51 | .editbar .tabs > a:hover,
52 | .editbar .tabs > button:hover {
53 | background-color:rgba(0,0,0,0.9);
54 | color:#fff;
55 | }
56 | .editbar .tabs > .settings {
57 | border-width:0px;
58 | height:38px;
59 | width:08.3333% !important;
60 | padding:7px !important;
61 | }
62 | .button.small {
63 | font-size:11px;
64 | line-height:20px;
65 | padding:5px 10px;
66 | font-weight:bold;
67 | height:34px;
68 | }
69 |
70 | .loading:before {
71 | background-color:rgba(0,0,0,0.25);
72 | z-index:100;
73 | }
74 | .loading:after { position:fixed; }
75 | /* Base candidates
76 | ------------------------- */
77 | .row-60 { height:60px; }
78 | .space-top6 { top:60px; }
79 | .scroll-styled { height:100%; }
80 | .z1000 { z-index:1000; }
81 | .z10000 { z-index:10000; }
82 |
83 | .scroll-styled::-webkit-scrollbar {
84 | width:5px;
85 | height:5px;
86 | border-left:0;
87 | background:rgba(0,0,0,0.1);
88 | }
89 | .scroll-styled::-webkit-scrollbar-track {
90 | background:none;
91 | }
92 | .scroll-styled::-webkit-scrollbar-thumb {
93 | background:rgba(0,0,0,0.4);
94 | border-radius:0;
95 | }
96 | .scroll-styled::-webkit-scrollbar:hover {
97 | background:rgba(0,0,0,0.4);
98 | }
99 |
100 | .uppercase { text-transform:uppercase; }
101 | .underline { text-decoration:underline; }
102 |
103 | .no-select { user-select: none; }
104 |
105 | /* Layout
106 | ------------------------- */
107 | .sidebar.active ~ .main { width:83.3334%; left:16.6666%; }
108 | .map {
109 | width:100%;
110 | height:100%;
111 | bottom:0;
112 | }
113 |
114 | /* Colors
115 | ------------------------- */
116 | button,
117 | .button,
118 | [type=button],
119 | [type=submit] {
120 | background-color:#6E65B3;
121 | }
122 | button:hover,
123 | .button:hover,
124 | [type=button]:hover,
125 | [type=submit]:hover {
126 | background-color:#534c87;
127 | }
128 |
129 | .progress,
130 | header .button.active,
131 | header .button.active:hover,
132 | .fill-purple { background-color:#6E65B3; }
133 | .area { fill:rgb(110, 101, 179); }
134 |
135 | /* Components
136 | ------------------------- */
137 | .fancy.title {
138 | font-size:22px;
139 | line-height:24px;
140 | margin:0;
141 | }
142 | header .fancy.title {
143 | line-height:60px;
144 | color:rgba(0,0,0,0.75);
145 | }
146 |
147 | header nav .button {
148 | background-color:transparent;
149 | color:rgba(0,0,0,0.5);
150 | }
151 | header .button:hover {
152 | background-color:rgba(0,0,0,0.05);
153 | color:rgba(0,0,0,0.75);
154 | }
155 | header .button.active {
156 | color:#fff;
157 | }
158 | header a {
159 | color:#6E65B3;
160 | }
161 | header a:hover {
162 | color:#26198A;
163 | }
164 |
165 | .sidebar-toggle:hover,
166 | .sidebar-toggle.active {
167 | background-color:rgba(0,0,0,0.05);
168 | }
169 | .sidebar-toggle.active {
170 | border-color:rgba(0,0,0,0.25);
171 | }
172 |
173 | .sidebar nav a:hover {
174 | background-color:rgba(0,0,0,0.20);
175 | }
176 | .sidebar nav a.active {
177 | background-color:rgba(0,0,0,0.5);
178 | color:#fff;
179 | }
180 |
181 | .dialog .close {
182 | background-color:transparent;
183 | color:rgba(0,0,0,0.5);
184 | }
185 | .dialog .close:hover {
186 | background-color:transparent;
187 | color:rgba(0,0,0,0.75);
188 | }
189 |
190 | .avatar-wrap {
191 | border: 2px solid rgba(255, 255, 255, 0.25);
192 | border-radius: 20px;
193 | margin-top: -2px;
194 | }
195 | .avatar {
196 | width: 20px;
197 | margin-right:5px !important;
198 | }
199 |
200 | .progress-bar {
201 | border-radius:50px;
202 | height:10px;
203 | }
204 | .progress {
205 | border-radius:50px;
206 | width:0;
207 | height:10px;
208 | top:0;
209 | left:0;
210 | right:0;
211 | min-width:1%;
212 | max-width:100%;
213 | }
214 |
215 | .rows > * { margin-bottom:1px; }
216 | .rows > *:first-child { border-radius:3px 3px 0 0; }
217 |
218 | .editor-key { width:80px; text-align:center; }
219 |
220 | .contributions > div {
221 | border-bottom:1px solid rgba(0,0,0,0.25);
222 | }
223 | .contributions > div:first-child { border-width:2px; }
224 | .contributions > div:last-child { border-bottom:none; }
225 |
226 | .user-menu {
227 | position:relative;
228 | top:0px;
229 | right:0px;
230 | padding:10px 0px 10px 0px;
231 | border:1px solid rgba(26,53,71,.12);
232 | border-radius:4px;
233 | box-shadow:0 1px 2px rgba(26,53,71,.1);
234 | opacity:0;
235 | pointer-events:none;
236 | -webkit-transform:scale(.8) translateY(-30%);
237 | transform:scale(.8) translateY(-30%);
238 | transition:.4s cubic-bezier(.3, 0, 0, 1.3);
239 | background:white;
240 | z-index:1000;
241 | width:130px;
242 | }
243 |
244 | .user-menu.visible {
245 | opacity:1;
246 | pointer-events:auto;
247 | -webkit-transform:none;
248 | transform:none;
249 | }
250 |
251 | .user-menu::before {
252 | content:"";
253 | position:absolute;
254 | top:-7px;
255 | left:calc(50% - 7px);
256 | width:13px;
257 | height:7px;
258 | background:url(user-menu-arrow.svg);
259 | }
260 |
261 | .user-menu li {
262 | list-style:none;
263 | text-align:left;
264 | }
265 |
266 | .select-container {
267 | display: inline-flex;
268 | position: relative;
269 | color: #fff;
270 | align-items: center;
271 | }
272 |
273 | .select {
274 | appearance: none;
275 | line-height: inherit;
276 | font-size: inherit;
277 | font-weight: bold;
278 | color: white;
279 | padding: 6px 30px 6px 12px;
280 | cursor: pointer;
281 | display: inline-block;
282 | border-radius: 4px;
283 | background-color: rgba(0, 0, 0, 0.25);
284 | }
285 |
286 | .select-arrow {
287 | position: absolute;
288 | right: 12px;
289 | top: 50%;
290 | pointer-events: none;
291 | border-left: 4px solid transparent;
292 | border-right: 4px solid transparent;
293 | border-top: 5px solid white;
294 | width: 8px;
295 | height: 8px;
296 | margin-top: -1px;
297 | }
298 |
299 | /* iD container
300 | * elements while editing
301 | ------------------------- */
302 | .ideditor {
303 | position:absolute;
304 | top:0;
305 | left:0;
306 | height:100%;
307 | width:100%;
308 | z-index:2000;
309 | }
310 | .ideditor-done {
311 | position:absolute;
312 | right:10px;
313 | top:10px;
314 | color:#fff;
315 | font-weight:bold;
316 | }
317 |
318 | /* D3 Dashboards
319 | ------------------------- */
320 | svg {
321 | font-size:10px;
322 | font-weight:bold;
323 | }
324 | text {
325 | font-family:Consolas, monospace;
326 | fill:rgba(255,255,255,0.5);
327 | }
328 | .axis path,
329 | .axis line {
330 | fill:none;
331 | stroke:transparent;
332 | shape-rendering:crispEdges;
333 | }
334 | .brush .extent {
335 | stroke:#fff;
336 | stroke-dasharray:4px 2px;
337 | fill-opacity:0.10;
338 | shape-rendering:crispEdges;
339 | }
340 |
341 | /* Small Screen Layout
342 | ------------------------- */
343 | @media only screen and (max-width:900px) {
344 | .sidebar { width:33.3333%; }
345 | .sidebar.active ~ .main { width:66.6666%; left:33.3333%; }
346 | }
347 |
348 | /* Tablet Layout
349 | ------------------------- */
350 | @media only screen and (max-width:770px) {}
351 |
352 | /* Mobile Layout
353 | ------------------------- */
354 | @media only screen and (max-width:640px) {
355 | .sidebar { width:100%; }
356 | .sidebar.active ~ .main { width:0; left:100%; }
357 | }
358 |
--------------------------------------------------------------------------------
/public/user-menu-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
11 |
--------------------------------------------------------------------------------
/src/components/activity/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import d3 from 'd3';
4 |
5 | import { USER_PROFILE_URL } from '../../config';
6 | import TasksSelectors from '../../stores/tasks_selectors';
7 | import ActivitySelectors from '../../stores/activity_selectors';
8 | import ActivityActionCreators from '../../stores/activity_action_creators';
9 |
10 | const mapStateToProps = (state) => ({
11 | currentTaskId: TasksSelectors.getCurrentTaskId(state),
12 | currentTask: TasksSelectors.getCurrentTask(state),
13 | currentTaskExtent: TasksSelectors.getCurrentTaskExtent(state),
14 | activity: ActivitySelectors.getData(state),
15 | });
16 |
17 | const mapDispatchToProps = {
18 | fetchRecentActivity: ActivityActionCreators.fetchRecentActivity,
19 | };
20 |
21 | class Activity extends Component {
22 | state = {
23 | loadCount: 10,
24 | }
25 |
26 | resetLoadCount() {
27 | this.setState({
28 | loadCount: 10,
29 | });
30 | }
31 |
32 | loadMore() {
33 | this.setState({
34 | loadCount: this.state.loadCount + 10,
35 | });
36 | }
37 |
38 | fetchData() {
39 | const { currentTaskId, fetchRecentActivity } = this.props;
40 |
41 | fetchRecentActivity({ taskId: currentTaskId })
42 | .then(() => this.resetLoadCount());
43 | }
44 |
45 | componentDidMount() {
46 | this.fetchData();
47 | }
48 |
49 | componentDidUpdate(prevProps) {
50 | // Refetch activity when a new task is selected
51 | if (prevProps.currentTaskId !== this.props.currentTaskId) {
52 | this.fetchData();
53 | }
54 | }
55 |
56 | renderActivityList() {
57 | const { activity } = this.props;
58 | const { loadCount } = this.state;
59 |
60 | if (activity.length === 0) {
61 | return No recent activity found.;
62 | }
63 |
64 | return activity.slice(0, loadCount).map((data, i) => {
65 | const { time, key, action, editor, user } = data;
66 |
67 | const permalink = `key-${key}`;
68 | const profile = `${USER_PROFILE_URL}/${user}`;
69 |
70 | const dateDisplay = d3.time.format('%B %-d');
71 | const timeDisplay = d3.time.format('%-I:%-M%p');
72 |
73 | const actionDate = dateDisplay(new Date(time * 1000));
74 | const actionTime = timeDisplay(new Date(time * 1000));
75 |
76 | const mapAction = {
77 | 'edit': 'Edited',
78 | 'fixed': 'Fixed',
79 | 'skip': 'Skipped',
80 | 'noterror': 'Not an error',
81 | };
82 |
83 | return (
84 |
85 |
86 | {mapAction[action]}
87 |
88 |
94 |
95 | {actionDate}
96 | {actionTime}
97 |
98 |
99 | );
100 | });
101 | }
102 |
103 | renderLoadMoreButton() {
104 | const { activity } = this.props;
105 | const { loadCount } = this.state;
106 |
107 | if (activity.length === 0) return null;
108 |
109 | if (loadCount >= activity.length) {
110 | return ;
111 | } else {
112 | return ;
113 | }
114 | }
115 |
116 | render() {
117 | const { currentTask, currentTaskExtent } = this.props;
118 | const { fromDate, toDate } = currentTaskExtent;
119 |
120 | const taskName = currentTask.value.name;
121 | const activityList = this.renderActivityList();
122 | const loadMoreButton = this.renderLoadMoreButton();
123 |
124 | return (
125 |
126 |
127 |
128 |
{taskName}
129 |
130 | {`Task last updated on ${fromDate}.`}
131 |
132 |
133 |
134 | {activityList}
135 | {loadMoreButton}
136 |
137 |
138 |
139 | );
140 | }
141 | }
142 |
143 | Activity.propTypes = {
144 | currentTaskId: PropTypes.string.isRequired,
145 | currentTask: PropTypes.object.isRequired,
146 | currentTaskExtent: PropTypes.object.isRequired,
147 | activity: PropTypes.array.isRequired,
148 | fetchRecentActivity: PropTypes.func.isRequired,
149 | };
150 |
151 | Activity = connect(
152 | mapStateToProps,
153 | mapDispatchToProps
154 | )(Activity);
155 |
156 | export default Activity;
157 |
--------------------------------------------------------------------------------
/src/components/admin/edit_task.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import filesize from 'file-size';
4 |
5 | import { AsyncStatus } from '../../stores/async_action';
6 | import UserSelectors from '../../stores/user_selectors';
7 | import ModalsActionCreators from '../../stores/modals_action_creators';
8 |
9 | const mapStateToProps = (state) => ({
10 | token: UserSelectors.getToken(state),
11 | });
12 |
13 | const mapDispatchToProps = {
14 | openSuccessModal: ModalsActionCreators.openSuccessModal,
15 | };
16 |
17 | class EditTask extends Component {
18 | state = {
19 | name: '',
20 | description: '',
21 | changesetComment: '',
22 | file: null,
23 | }
24 |
25 | handleNameChange = (e) => {
26 | const name = e.target.value;
27 | this.setState({ name });
28 | }
29 |
30 | handleDescriptionChange = (e) => {
31 | const description = e.target.value;
32 | this.setState({ description });
33 | }
34 |
35 | handleChangesetCommentChange = (e) => {
36 | const changesetComment = e.target.value;
37 | this.setState({ changesetComment });
38 | }
39 |
40 | handleFileInputChange = (e) => {
41 | const file = e.target.files[0];
42 | this.setState({ file });
43 | }
44 |
45 | handleSubmit = (e) => {
46 | e.preventDefault();
47 |
48 | const { onSubmit, token } = this.props;
49 | const formData = this.getFormData();
50 |
51 | onSubmit({ token, payload: formData })
52 | .then(response => {
53 | if (response.status === AsyncStatus.SUCCESS) {
54 | this.props.openSuccessModal('Task updated succesfully');
55 | }
56 | });
57 | }
58 |
59 | getFormData = () => {
60 | const { task } = this.props;
61 | const { name, description, changesetComment, file } = this.state;
62 |
63 | const formData = new window.FormData();
64 |
65 | formData.append('idtask', task.idtask);
66 | formData.append('name', name);
67 | formData.append('description', description);
68 | formData.append('changesetComment', changesetComment);
69 | formData.append('isCompleted', false);
70 | if (file) {
71 | formData.append('file', file);
72 | }
73 |
74 | return formData;
75 | }
76 |
77 | setInitialState = () => {
78 | const { task } = this.props;
79 |
80 | this.setState({
81 | name: task.value.name,
82 | description: task.value.description,
83 | changesetComment: task.value.changesetComment,
84 | });
85 | }
86 |
87 | componentDidMount() {
88 | this.setInitialState();
89 | }
90 |
91 | shouldComponentUpdate(nextProps) {
92 | return (
93 | nextProps.task.idtask !== this.props.idtask
94 | );
95 | }
96 |
97 | componentDidUpdate(prevProps) {
98 | if (prevProps.task.idtask !== this.props.task.idtask) {
99 | this.setInitialState();
100 | }
101 | }
102 |
103 | render() {
104 | const { name, description, changesetComment, file } = this.state;
105 | const { onCancel } = this.props;
106 |
107 | return (
108 |
157 | );
158 | }
159 | }
160 |
161 | EditTask.propTypes = {
162 | task: PropTypes.object.isRequired,
163 | onSubmit: PropTypes.func.isRequired,
164 | onCancel: PropTypes.func.isRequired,
165 | openSuccessModal: PropTypes.func.isRequired,
166 | };
167 |
168 | EditTask = connect(
169 | mapStateToProps,
170 | mapDispatchToProps,
171 | )(EditTask);
172 |
173 | export default EditTask;
174 |
--------------------------------------------------------------------------------
/src/components/admin/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { ROLES } from '../../constants/user_constants';
5 | import TasksSelectors from '../../stores/tasks_selectors';
6 | import UserSelectors from '../../stores/user_selectors';
7 | import TasksActionCreators from '../../stores/tasks_action_creators';
8 |
9 | import ShowTask from './show_task';
10 | import EditTask from './edit_task';
11 |
12 | const mapStateToProps = (state) => ({
13 | currentTaskId: TasksSelectors.getCurrentTaskId(state),
14 | currentTask: TasksSelectors.getCurrentTask(state),
15 | currentTaskExtent: TasksSelectors.getCurrentTaskExtent(state),
16 | isAuthenticated: UserSelectors.getIsAuthenticated(state),
17 | role: UserSelectors.getRole(state),
18 | userId: UserSelectors.getOsmId(state),
19 | });
20 |
21 | const mapDispatchToProps = {
22 | updateTask: TasksActionCreators.updateTask,
23 | };
24 |
25 | class Admin extends Component {
26 | state = {
27 | taskWindow: 'show', // 'show' | 'edit'
28 | }
29 |
30 | setTaskWindow = (to) => {
31 | this.setState({
32 | taskWindow: to,
33 | });
34 | }
35 |
36 | componentDidUpdate(prevProps) {
37 | if (prevProps.currentTaskId !== this.props.currentTaskId) {
38 | this.setTaskWindow('show');
39 | }
40 | }
41 |
42 | render() {
43 | const { taskWindow } = this.state;
44 | const { currentTask, updateTask, currentTaskExtent, role, isAuthenticated, userId } = this.props;
45 |
46 | const taskName = currentTask.value.name;
47 | const fromDate = currentTaskExtent.fromDate;
48 |
49 | if (!isAuthenticated) {
50 | return (
51 |
52 |
Please login to access the admin section.
53 |
54 | );
55 | }
56 |
57 | if (role === ROLES.EDITOR) {
58 | return (
59 |
60 |
You need admin privileges to see this section.
61 |
62 | );
63 | }
64 |
65 | const canEdit = (role === ROLES.SUPERADMIN || (role === ROLES.ADMIN && currentTask.iduser === userId));
66 |
67 | return (
68 |
69 |
70 |
{taskName}
71 |
72 | {`Task last updated on ${fromDate}.`}
73 |
74 |
75 |
76 | {(taskWindow === 'show') ? this.setTaskWindow('edit')}/> : null}
77 | {(taskWindow === 'edit') ? this.setTaskWindow('show')} onSubmit={updateTask} /> : null}
78 |
79 |
80 | );
81 | }
82 | }
83 |
84 | Admin.propTypes = {
85 | currentTaskId: PropTypes.string.isRequired,
86 | currentTask: PropTypes.object.isRequired,
87 | currentTaskExtent: PropTypes.object.isRequired,
88 | isAuthenticated: PropTypes.bool.isRequired,
89 | role: PropTypes.string,
90 | userId: PropTypes.string,
91 | updateTask: PropTypes.func.isRequired,
92 | };
93 |
94 | Admin = connect(
95 | mapStateToProps,
96 | mapDispatchToProps
97 | )(Admin);
98 |
99 | export default Admin;
100 |
--------------------------------------------------------------------------------
/src/components/admin/show_task.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import d3 from 'd3';
3 |
4 | const ShowTask = ({ task, canEdit, onEdit }) => {
5 | const dateDisplay = d3.time.format('%B %-d');
6 | const timeDisplay = d3.time.format('%-I:%-M%p');
7 |
8 | const updatedDay = dateDisplay(new Date(task.value.updated * 1000));
9 | const updatedTime = timeDisplay(new Date(task.value.updated * 1000));
10 |
11 | const status = (task.isCompleted) ? 'Completed.' : 'Items remaining to be done.';
12 |
13 | const editButtonClass = canEdit ? '' : 'disabled';
14 |
15 | return (
16 |
17 |
18 |
19 | Title
20 |
21 |
22 | {task.value.name}
23 |
24 |
25 |
26 |
27 | Task ID
28 |
29 |
30 | {task.idtask}
31 |
32 |
33 |
34 |
35 | Description
36 |
37 |
38 | {task.value.description}
39 |
40 |
41 |
42 |
43 | Changeset comment
44 |
45 |
46 | {task.value.changesetComment}
47 |
48 |
49 |
50 |
51 | Updated
52 |
53 |
54 | {updatedDay}
55 | {' '}
56 | {updatedTime}
57 |
58 |
59 |
60 |
61 | Status
62 |
63 |
64 | {status}
65 |
66 |
67 |
68 |
69 |
70 |
71 | );
72 | }
73 |
74 | ShowTask.propTypes = {
75 | task: PropTypes.object.isRequired,
76 | onEdit: PropTypes.func.isRequired,
77 | };
78 |
79 | export default ShowTask;
80 |
--------------------------------------------------------------------------------
/src/components/app/app_header.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { connect } from 'react-redux';
4 |
5 | import SettingsActionCreators from '../../stores/settings_action_creators';
6 | import SettingsSelectors from '../../stores/settings_selectors';
7 | import TasksSelectors from '../../stores/tasks_selectors';
8 |
9 | import Login from '../shared/login';
10 |
11 | const mapStateToProps = (state) => ({
12 | currentTaskId: TasksSelectors.getCurrentTaskId(state),
13 | sidebar: SettingsSelectors.getSidebarSetting(state),
14 | });
15 |
16 | const mapDispatchToProps = {
17 | toggleSidebar: SettingsActionCreators.toggleSidebar,
18 | };
19 |
20 | class Header extends Component {
21 | toggleSidebar = (e) => {
22 | e.preventDefault();
23 | this.props.toggleSidebar();
24 | }
25 |
26 | render() {
27 | const { currentTaskId, sidebar } = this.props;
28 |
29 | const isActive = sidebar ? 'active' : '';
30 | const toggleClass = `sidebar-toggle quiet block fl keyline-right animate pad1 row-60 ${isActive}`;
31 |
32 | return (
33 |
34 |
42 |
43 |
63 |
64 |
65 |
66 |
67 |
68 | );
69 | }
70 | }
71 |
72 | Header.propTypes = {
73 | currentTaskId: PropTypes.string.isRequired,
74 | sidebar: PropTypes.bool.isRequired,
75 | toggleSidebar: PropTypes.func.isRequired,
76 | };
77 |
78 | Header = connect(
79 | mapStateToProps,
80 | mapDispatchToProps
81 | )(Header);
82 |
83 | export default Header;
84 |
--------------------------------------------------------------------------------
/src/components/app/app_sidebar.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Link, withRouter } from 'react-router';
3 | import { connect } from 'react-redux';
4 |
5 | import { ROLES } from '../../constants/user_constants';
6 | import UserSelectors from '../../stores/user_selectors';
7 | import TasksSelectors from '../../stores/tasks_selectors';
8 | import SettingsSelectors from '../../stores/settings_selectors';
9 | import ModalsActionCreators from '../../stores/modals_action_creators';
10 |
11 | const mapStateToProps = (state, { routes }) => ({
12 | topLevelRoute: routes[1].name,
13 | role: UserSelectors.getRole(state),
14 | userId: UserSelectors.getOsmId(state),
15 | sidebar: SettingsSelectors.getSidebarSetting(state),
16 | activeTasks: TasksSelectors.getActiveTasks(state),
17 | completedTasks: TasksSelectors.getCompletedTasks(state),
18 | });
19 |
20 | const mapDispatchToProps = {
21 | openCreateTaskModal: ModalsActionCreators.openCreateTaskModal,
22 | };
23 |
24 | class Sidebar extends Component {
25 | renderCreateTaskBtn() {
26 | const { role, topLevelRoute, openCreateTaskModal } = this.props;
27 |
28 | if (role === ROLES.ADMIN || role === ROLES.SUPERADMIN) {
29 | if (topLevelRoute === "admin") {
30 | return (
31 |
32 |
35 |
36 | );
37 | }
38 | }
39 |
40 | return null;
41 | }
42 |
43 | renderTaskList(tasks) {
44 | const { role, userId, topLevelRoute } = this.props;
45 |
46 | if (tasks.length === 0) {
47 | return No tasks.
;
48 | };
49 |
50 | return tasks.map((task, i) => {
51 | let iconClass = '';
52 | if (topLevelRoute === "admin") {
53 | if (role === ROLES.SUPERADMIN || (role === ROLES.ADMIN && task.iduser === userId)) {
54 | iconClass = 'icon pencil';
55 | } else {
56 | iconClass = 'icon lock';
57 | }
58 | }
59 |
60 | return (
61 |
67 | {task.value.name}
68 |
69 | );
70 | });
71 | }
72 |
73 | render() {
74 | const { sidebar, activeTasks, completedTasks } = this.props;
75 |
76 | const isActive = sidebar ? 'active' : '';
77 | const sidebarClass = `sidebar pin-bottomleft clip col2 animate offcanvas-left fill-navy space-top6 ${isActive}`;
78 |
79 | const createTaskBtn = this.renderCreateTaskBtn();
80 | const activeTasksList = this.renderTaskList(activeTasks);
81 | const completedTasksList = this.renderTaskList(completedTasks);
82 |
83 | return (
84 |
85 |
86 | {createTaskBtn}
87 |
Current Tasks
88 |
89 | Completed Tasks
90 |
91 |
92 |
93 | );
94 | }
95 | }
96 |
97 | Sidebar.propTypes = {
98 | topLevelRoute: PropTypes.string.isRequired,
99 | role: PropTypes.string,
100 | userId: PropTypes.string,
101 | sidebar: PropTypes.bool.isRequired,
102 | activeTasks: PropTypes.array.isRequired,
103 | completedTasks: PropTypes.array.isRequired,
104 | openCreateTaskModal: PropTypes.func.isRequired,
105 | };
106 |
107 | Sidebar = withRouter(connect(
108 | mapStateToProps,
109 | mapDispatchToProps
110 | )(Sidebar));
111 |
112 | export default Sidebar;
113 |
--------------------------------------------------------------------------------
/src/components/app/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { withRouter } from 'react-router';
4 |
5 | import { AsyncStatus } from '../../stores/async_action';
6 | import TasksActionCreators from '../../stores/tasks_action_creators';
7 | import TasksSelectors from '../../stores/tasks_selectors';
8 | import LoadingSelectors from '../../stores/loading_selectors';
9 |
10 | import AppHeader from './app_header';
11 | import AppSidebar from './app_sidebar';
12 | import SettingsModal from '../shared/settings_modal';
13 | import SuccessModal from '../shared/success_modal';
14 | import ErrorModal from '../shared/error_modal';
15 | import CreateTaskModal from '../shared/create_task_modal';
16 | import ManageUsersModal from '../shared/manage_users_modal';
17 |
18 | const mapStateToProps = (state) => ({
19 | currentTaskId: TasksSelectors.getCurrentTaskId(state),
20 | latestTaskId: TasksSelectors.getLatestTaskId(state),
21 | currentTask: TasksSelectors.getCurrentTask(state),
22 | isLoading: LoadingSelectors.getIsLoading(state),
23 | });
24 |
25 | const mapDispatchToProps = {
26 | fetchAllTasks: TasksActionCreators.fetchAllTasks,
27 | };
28 |
29 | class App extends Component {
30 | state = {
31 | appLoading: false,
32 | errorMessage: '',
33 | }
34 |
35 | componentDidMount() {
36 | this.fetchData();
37 | }
38 |
39 | fetchData() {
40 | const { fetchAllTasks } = this.props;
41 | this.setState({ appLoading: true });
42 | fetchAllTasks()
43 | .then(response => {
44 | this.setState({ appLoading: false });
45 |
46 | if (response.status === AsyncStatus.FAILURE) {
47 | this.setState({
48 | errorMessage: response.error.message || 'Something went wrong.',
49 | });
50 | }
51 | });
52 | }
53 |
54 | componentDidUpdate() {
55 | const { currentTaskId, latestTaskId, router } = this.props;
56 | if (!currentTaskId && latestTaskId) {
57 | router.push(`/task/${latestTaskId}`);
58 | }
59 | }
60 |
61 | render() {
62 | const { currentTask, isLoading } = this.props;
63 | const { appLoading, errorMessage } = this.state;
64 |
65 | const appLoadingClass = appLoading ? 'loading' : '';
66 | const componentLoadingClass = isLoading ? 'loading' : '';
67 |
68 | return (
69 |
70 | {errorMessage &&
71 |
{errorMessage}
72 | }
73 | {currentTask &&
74 |
75 |
76 |
77 | {this.props.children}
78 |
79 |
80 |
81 |
82 |
83 |
84 |
}
85 |
86 | );
87 | }
88 | }
89 |
90 | App.propTypes = {
91 | router: PropTypes.object.isRequired,
92 | currentTaskId: PropTypes.string,
93 | latestTaskId: PropTypes.string,
94 | currentTask: PropTypes.object,
95 | isLoading: PropTypes.bool.isRequired,
96 | fetchAllTasks: PropTypes.func.isRequired,
97 | };
98 |
99 | App = withRouter(connect(
100 | mapStateToProps,
101 | mapDispatchToProps,
102 | )(App));
103 |
104 | export default App;
105 |
--------------------------------------------------------------------------------
/src/components/shared/create_task_modal.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import filesize from 'file-size';
4 |
5 | import { AsyncStatus } from '../../stores/async_action';
6 | import UserSelectors from '../../stores/user_selectors';
7 | import ModalsSelectors from '../../stores/modals_selectors';
8 | import ModalsActionCreators from '../../stores/modals_action_creators';
9 | import TasksActionCreators from '../../stores/tasks_action_creators';
10 |
11 | const mapStateToProps = (state) => ({
12 | token: UserSelectors.getToken(state),
13 | showCreateTaskModal: ModalsSelectors.getShowCreateTaskModal(state),
14 | });
15 |
16 | const mapDispatchToProps = {
17 | closeCreateTaskModal: ModalsActionCreators.closeCreateTaskModal,
18 | createTask: TasksActionCreators.createTask,
19 | };
20 |
21 | class CreateTaskModal extends Component {
22 | initialState = {
23 | name: '',
24 | description: '',
25 | changesetComment: '',
26 | file: {},
27 | isLoading: false,
28 | isSuccess: false,
29 | isFailure: false,
30 | errorMessage: '',
31 | }
32 |
33 | state = this.initialState
34 |
35 | resetFormState = () => {
36 | this.setState({
37 | name: '',
38 | description: '',
39 | changesetComment: '',
40 | file: {},
41 | });
42 | }
43 |
44 | resetAsyncState = () => {
45 | this.setState({
46 | isLoading: false,
47 | isSuccess: false,
48 | isFailure: false,
49 | errorMessage: '',
50 | });
51 | }
52 |
53 | handleNameChange = (e) => {
54 | const name = e.target.value;
55 | this.setState({ name });
56 | }
57 |
58 | handleDescriptionChange = (e) => {
59 | const description = e.target.value;
60 | this.setState({ description });
61 | }
62 |
63 | handleChangesetCommentChange = (e) => {
64 | const changesetComment = e.target.value;
65 | this.setState({ changesetComment });
66 | }
67 |
68 | handleFileInputChange = (e) => {
69 | const file = e.target.files[0];
70 | this.setState({ file });
71 | }
72 |
73 | handleSubmit = (e) => {
74 | e.preventDefault();
75 |
76 | const { createTask, token } = this.props;
77 | const formData = this.getFormData();
78 |
79 | this.resetAsyncState();
80 | this.setState({ isLoading: true });
81 |
82 | createTask({ token, payload: formData })
83 | .then(response => {
84 | this.setState({ isLoading: false });
85 |
86 | if (response.status === AsyncStatus.SUCCESS) {
87 | this.resetFormState();
88 | this.setState({
89 | isSuccess: true,
90 | });
91 | } else {
92 | this.setState({
93 | isFailure: true,
94 | errorMessage: response.error,
95 | });
96 | }
97 | });
98 | }
99 |
100 | getFormData = () => {
101 | const { name, description, changesetComment, file } = this.state;
102 |
103 | const formData = new window.FormData();
104 |
105 | formData.append('name', name);
106 | formData.append('description', description);
107 | formData.append('changesetComment', changesetComment);
108 | formData.append('file', file);
109 |
110 | return formData;
111 | }
112 |
113 | stopProp = (e) => {
114 | e.stopPropagation();
115 | e.nativeEvent.stopImmediatePropagation();
116 | }
117 |
118 | renderNotice = () => {
119 | const { isSuccess, isFailure, errorMessage } = this.state;
120 |
121 | if (isSuccess) {
122 | return (
123 |
124 |
Success
125 |
Task created successfully.
126 |
127 | );
128 | }
129 |
130 | if (isFailure) {
131 | return (
132 |
133 |
Error
134 |
{errorMessage || 'Something went wrong.'}
135 |
136 | );
137 | }
138 |
139 | return null;
140 | }
141 |
142 | renderForm = () => {
143 | const { name, description, changesetComment, file } = this.state;
144 |
145 | return (
146 |
198 | );
199 | }
200 |
201 | render() {
202 | const { showCreateTaskModal, closeCreateTaskModal } = this.props;
203 |
204 | const notice = this.renderNotice();
205 | const form = this.renderForm();
206 |
207 | if (showCreateTaskModal) {
208 | const loadingClass = this.state.isLoading ? 'loading' : '';
209 | const modalClass = `animate modal modal-content active ${loadingClass}`;
210 |
211 | return (
212 |
213 |
214 |
215 |
216 |
Create a new task
217 |
218 | {notice}
219 | {form}
220 |
221 |
222 | );
223 | }
224 |
225 | return null;
226 | }
227 | }
228 |
229 | CreateTaskModal.propTypes = {
230 | token: PropTypes.string,
231 | showCreateTaskModal: PropTypes.bool.isRequired,
232 | closeCreateTaskModal: PropTypes.func.isRequired,
233 | createTask: PropTypes.func.isRequired,
234 | };
235 |
236 | CreateTaskModal = connect(
237 | mapStateToProps,
238 | mapDispatchToProps
239 | )(CreateTaskModal);
240 |
241 | export default CreateTaskModal;
242 |
--------------------------------------------------------------------------------
/src/components/shared/error_modal.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import KeyBinding from 'react-keybinding';
3 | import { connect } from 'react-redux';
4 |
5 | import ModalsActionCreators from '../../stores/modals_action_creators';
6 | import ModalsSelectors from '../../stores/modals_selectors';
7 |
8 | const mapStateToProps = (state) => ({
9 | showErrorModal: ModalsSelectors.getShowErrorModal(state),
10 | errorMessage: ModalsSelectors.getErrorMessage(state),
11 | });
12 |
13 | const mapDispatchToProps = {
14 | closeErrorModal: ModalsActionCreators.closeErrorModal,
15 | };
16 |
17 | let ErrorModal = React.createClass({
18 | propTypes: {
19 | showErrorModal: PropTypes.bool.isRequired,
20 | errorMessage: PropTypes.string,
21 | closeErrorModal: PropTypes.func.isRequired,
22 | },
23 |
24 | mixins: [KeyBinding],
25 |
26 | keybindings: {
27 | 'esc': function(e) {
28 | this.props.closeErrorModal();
29 | },
30 | },
31 |
32 | render() {
33 | const { showErrorModal, errorMessage, closeErrorModal } = this.props;
34 |
35 | const isActive = showErrorModal ? 'active' : '';
36 | const modalClass = `animate modal modal-content ${isActive}`;
37 |
38 | return (
39 |
55 | );
56 | }
57 | });
58 |
59 | ErrorModal = connect(
60 | mapStateToProps,
61 | mapDispatchToProps
62 | )(ErrorModal);
63 |
64 | export default ErrorModal;
65 |
--------------------------------------------------------------------------------
/src/components/shared/login.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { Link } from 'react-router';
3 | import { connect } from 'react-redux';
4 |
5 | import { ROLES } from '../../constants/user_constants';
6 | import { server } from '../../services';
7 | import UserActionCreators from '../../stores/user_action_creators';
8 | import UserSelectors from '../../stores/user_selectors';
9 | import ModalsActionCreators from '../../stores/modals_action_creators';
10 | import TasksSelectors from '../../stores/tasks_selectors';
11 |
12 | const mapStateToProps = (state) => ({
13 | isAuthenticated: UserSelectors.getIsAuthenticated(state),
14 | username: UserSelectors.getUsername(state),
15 | osmid: UserSelectors.getOsmId(state),
16 | avatar: UserSelectors.getAvatar(state),
17 | token: UserSelectors.getToken(state),
18 | role: UserSelectors.getRole(state),
19 | currentTaskId: TasksSelectors.getCurrentTaskId(state),
20 | });
21 |
22 | const mapDispatchToProps = {
23 | login: UserActionCreators.login,
24 | logout: UserActionCreators.logout,
25 | fetchUserDetails: UserActionCreators.fetchUserDetails,
26 | openSettingsModal: ModalsActionCreators.openSettingsModal,
27 | openManageUsersModal: ModalsActionCreators.openManageUsersModal,
28 | };
29 |
30 | class Login extends Component {
31 | state = {
32 | showUserMenu: false,
33 | }
34 |
35 | componentDidMount() {
36 | const { token, fetchUserDetails } = this.props;
37 | if (token) fetchUserDetails({ token });
38 | }
39 |
40 | onLoginClick = () => {
41 | const { login } = this.props;
42 |
43 | const popup = this.createPopup(600, 550, 'oauth_popup');
44 | popup.location = server.loginURL;
45 |
46 | window.authComplete = (location) => {
47 | const queryString = location.split('?')[1];
48 | const creds = this.parseQueryString(queryString);
49 | login(creds);
50 |
51 | delete window.authComplete;
52 | }
53 | }
54 |
55 | openUserMenu = (e) => {
56 | e.preventDefault();
57 | this.toggleUserMenu();
58 | }
59 |
60 | openSettingsModal = (e) => {
61 | e.preventDefault();
62 | this.toggleUserMenu();
63 | this.props.openSettingsModal();
64 | }
65 |
66 | openManageUsersModal = (e) => {
67 | e.preventDefault();
68 | this.toggleUserMenu();
69 | this.props.openManageUsersModal();
70 | }
71 |
72 | logout = (e) => {
73 | e.preventDefault();
74 | this.toggleUserMenu();
75 | const { token } = this.props;
76 | this.props.logout({ token });
77 | }
78 |
79 | toggleUserMenu = (e) => {
80 | this.setState({
81 | showUserMenu: !this.state.showUserMenu,
82 | });
83 | }
84 |
85 | createPopup(width, height, title) {
86 | const settings = [
87 | ['width', width], ['height', height],
88 | ['left', screen.width / 2 - width / 2],
89 | ['top', screen.height / 2 - height / 2]
90 | ].map(x => x.join('='))
91 | .join(',');
92 |
93 | const popup = window.open('about:blank', title, settings);
94 | return popup;
95 | }
96 |
97 | parseQueryString(queryString) {
98 | const query = {};
99 |
100 | queryString.split('&').forEach(pair => {
101 | const [key, value] = pair.split('=');
102 | query[decodeURIComponent(key)] = decodeURIComponent(value) || null;
103 | });
104 |
105 | return query;
106 | }
107 |
108 | renderLoginState() {
109 | const { isAuthenticated, username, avatar, role, currentTaskId } = this.props;
110 |
111 | const menuClass = `user-menu ${this.state.showUserMenu ? 'visible' : 'hidden'}`;
112 |
113 | if (isAuthenticated) {
114 | return (
115 |
116 |
122 |
123 | -
124 | Settings
125 |
126 | {
127 | (role === ROLES.ADMIN || role === ROLES.SUPERADMIN)
128 | ? -
129 |
133 | Manage tasks
134 |
135 |
136 | : null
137 | }
138 | {
139 | (role === ROLES.SUPERADMIN)
140 | ? -
141 | Manage users
142 |
143 | : null
144 | }
145 | -
146 | Logout
147 |
148 |
149 |
150 | );
151 | }
152 |
153 | return (
154 |
155 |
156 |
157 | );
158 | }
159 |
160 | render() {
161 | const loginState = this.renderLoginState();
162 |
163 | return (
164 |
165 | {loginState}
166 |
167 | );
168 | }
169 | }
170 |
171 | Login.propTypes = {
172 | isAuthenticated: PropTypes.bool.isRequired,
173 | username: PropTypes.string,
174 | osmid: PropTypes.string,
175 | avatar: PropTypes.string,
176 | token: PropTypes.string,
177 | role: PropTypes.string,
178 | login: PropTypes.func.isRequired,
179 | logout: PropTypes.func.isRequired,
180 | fetchUserDetails: PropTypes.func.isRequired,
181 | openSettingsModal: PropTypes.func.isRequired,
182 | openManageUsersModal: PropTypes.func.isRequired,
183 | currentTaskId: PropTypes.string.isRequired,
184 | };
185 |
186 | Login = connect(
187 | mapStateToProps,
188 | mapDispatchToProps
189 | )(Login);
190 |
191 | export default Login;
192 |
193 | /*
194 |
195 |
196 |
197 |
198 |
199 | {username}
200 |
201 |
202 | -
203 | Settings
204 |
205 | {
206 | (role === ROLES.ADMIN || role === ROLES.SUPERADMIN)
207 | ? -
208 |
212 | Manage tasks
213 |
214 |
215 | : null
216 | }
217 | {
218 | (role === ROLES.SUPERADMIN)
219 | ? -
220 | Manage users
221 |
222 | : null
223 | }
224 | -
225 | Logout
226 |
227 |
228 |
229 |
230 | */
231 |
--------------------------------------------------------------------------------
/src/components/shared/manage_users_modal.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 |
4 | import { USER_PROFILE_URL } from '../../config';
5 | import { ROLES } from '../../constants/user_constants';
6 | import { AsyncStatus } from '../../stores/async_action';
7 | import UserSelectors from '../../stores/user_selectors';
8 | import ModalsSelectors from '../../stores/modals_selectors';
9 | import ModalsActionCreators from '../../stores/modals_action_creators';
10 | import AdminSelectors from '../../stores/admin_selectors';
11 | import AdminActionCreators from '../../stores/admin_action_creators';
12 |
13 | const mapStateToProps = (state) => ({
14 | showManageUsersModal: ModalsSelectors.getShowManageUsersModal(state),
15 | users: AdminSelectors.getUsers(state),
16 | token: UserSelectors.getToken(state),
17 | });
18 |
19 | const mapDispatchToProps = {
20 | closeManageUsersModal: ModalsActionCreators.closeManageUsersModal,
21 | fetchAllUsers: AdminActionCreators.fetchAllUsers,
22 | changeUserRole: AdminActionCreators.changeUserRole,
23 | };
24 |
25 | class ManageUsersModal extends Component {
26 | initialState = {
27 | isLoading: false,
28 | isSuccess: false,
29 | isFailure: false,
30 | errorMessage: '',
31 | }
32 |
33 | state = this.initialState
34 |
35 | resetAsyncState = () => {
36 | this.setState(this.initialState);
37 | }
38 |
39 | componentDidUpdate() {
40 | const { showManageUsersModal, users } = this.props;
41 |
42 | if (showManageUsersModal && users.length === 0) {
43 | const { fetchAllUsers, token } = this.props;
44 | fetchAllUsers({ token })
45 | }
46 | }
47 |
48 | stopProp = (e) => {
49 | e.stopPropagation();
50 | e.nativeEvent.stopImmediatePropagation();
51 | }
52 |
53 | handleChange = (e, userID) => {
54 | e.preventDefault();
55 |
56 | const { changeUserRole, token } = this.props;
57 |
58 | this.resetAsyncState();
59 | this.setState({ isLoading: true });
60 |
61 | changeUserRole({token, payload: { iduser: userID, role: e.target.value }})
62 | .then(response => {
63 | this.setState({ isLoading: false });
64 |
65 | if (response.status === AsyncStatus.SUCCESS) {
66 | this.setState({
67 | isSuccess: true,
68 | });
69 | } else {
70 | this.setState({
71 | isFailure: true,
72 | errorMessage: response.error,
73 | });
74 | }
75 | });
76 | }
77 |
78 | renderNotice = () => {
79 | const { isSuccess, isFailure, errorMessage } = this.state;
80 |
81 | if (isSuccess) {
82 | return (
83 |
84 |
Success
85 |
User role changed successfully.
86 |
87 | );
88 | }
89 |
90 | if (isFailure) {
91 | return (
92 |
93 |
Error
94 |
{errorMessage || 'Something went wrong.'}
95 |
96 | );
97 | }
98 |
99 | return null;
100 | }
101 |
102 | renderUser = ({ user: username, img, role, id }, key) => (
103 |
104 |
111 |
112 |
117 |
118 |
119 | );
120 |
121 | render() {
122 | const { showManageUsersModal, closeManageUsersModal, users } = this.props;
123 |
124 | if (showManageUsersModal && users.length !== 0) {
125 | const loadingClass = this.state.isLoading ? 'loading' : '';
126 | const modalClass = `animate modal modal-content active ${loadingClass}`;
127 |
128 | const notice = this.renderNotice();
129 |
130 | return (
131 |
132 |
133 |
134 |
135 |
Change user roles
136 |
137 | {notice}
138 |
139 | {users.map(this.renderUser)}
140 |
141 |
142 |
143 | );
144 | }
145 |
146 | return null;
147 | }
148 | }
149 |
150 | ManageUsersModal.propTypes = {
151 | showManageUsersModal: PropTypes.bool.isRequired,
152 | users: PropTypes.array.isRequired,
153 | token: PropTypes.string,
154 | closeManageUsersModal: PropTypes.func.isRequired,
155 | fetchAllUsers: PropTypes.func.isRequired,
156 | changeUserRole: PropTypes.func.isRequired,
157 | };
158 |
159 | ManageUsersModal = connect(
160 | mapStateToProps,
161 | mapDispatchToProps
162 | )(ManageUsersModal);
163 |
164 | export default ManageUsersModal;
165 |
--------------------------------------------------------------------------------
/src/components/shared/settings_modal.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import KeyBinding from 'react-keybinding';
3 | import { connect } from 'react-redux';
4 |
5 | import SettingsSelectors from '../../stores/settings_selectors';
6 | import ModalsSelectors from '../../stores/modals_selectors';
7 | import SettingsActionCreators from '../../stores/settings_action_creators';
8 | import ModalsActionCreators from '../../stores/modals_action_creators';
9 |
10 | const mapStateToProps = (state) => ({
11 | editor: SettingsSelectors.getEditorSetting(state),
12 | showSettingsModal: ModalsSelectors.getShowSettingsModal(state),
13 | });
14 |
15 | const mapDispatchToProps = {
16 | setEditorPreference: SettingsActionCreators.setEditorPreference,
17 | closeSettingsModal: ModalsActionCreators.closeSettingsModal,
18 | };
19 |
20 | let SettingsModal = React.createClass({
21 | propTypes: {
22 | editor: PropTypes.string.isRequired,
23 | showSettingsModal: PropTypes.bool.isRequired,
24 | setEditorPreference: PropTypes.func.isRequired,
25 | closeSettingsModal: PropTypes.func.isRequired,
26 | },
27 |
28 | mixins: [KeyBinding],
29 |
30 | keybindings: {
31 | 'esc': function(e) {
32 | this.props.closeSettingsModal();
33 | },
34 | },
35 |
36 | setEditor(e) {
37 | var editor = e.target.getAttribute('id');
38 | this.props.setEditorPreference(editor);
39 | },
40 |
41 | stopProp(e) {
42 | e.stopPropagation();
43 | e.nativeEvent.stopImmediatePropagation();
44 | },
45 |
46 | render() {
47 | const { editor, showSettingsModal, closeSettingsModal } = this.props;
48 |
49 | if (showSettingsModal) {
50 | return (
51 |
52 |
53 |
54 |
55 |
Settings
56 |
57 |
58 |
67 |
68 |
69 | Shortcut keys:
e
Edit s
Skip n
Not an error f
Fixed
70 |
*JOSM requires remote control to be set in preferences.
71 |
72 |
73 |
74 | );
75 | }
76 |
77 | return null;
78 | }
79 | });
80 |
81 | SettingsModal = connect(
82 | mapStateToProps,
83 | mapDispatchToProps
84 | )(SettingsModal);
85 |
86 | export default SettingsModal;
87 |
--------------------------------------------------------------------------------
/src/components/shared/success_modal.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import KeyBinding from 'react-keybinding';
3 | import { connect } from 'react-redux';
4 |
5 | import ModalsActionCreators from '../../stores/modals_action_creators';
6 | import ModalsSelectors from '../../stores/modals_selectors';
7 |
8 | const mapStateToProps = (state) => ({
9 | showSuccessModal: ModalsSelectors.getShowSuccessModal(state),
10 | successMessage: ModalsSelectors.getSuccessMessage(state),
11 | });
12 |
13 | const mapDispatchToProps = {
14 | closeSuccessModal: ModalsActionCreators.closeSuccessModal,
15 | };
16 |
17 | let SuccessModal = React.createClass({
18 | propTypes: {
19 | showSuccessModal: PropTypes.bool.isRequired,
20 | successMessage: PropTypes.string,
21 | closeSuccessModal: PropTypes.func.isRequired,
22 | },
23 |
24 | mixins: [KeyBinding],
25 |
26 | keybindings: {
27 | 'esc': function(e) {
28 | this.props.closeSuccessModal();
29 | },
30 | },
31 |
32 | render() {
33 | const { showSuccessModal, successMessage, closeSuccessModal } = this.props;
34 |
35 | const isActive = showSuccessModal ? 'active' : '';
36 | const modalClass = `animate modal modal-content ${isActive}`;
37 |
38 | return (
39 |
40 |
41 |
42 |
43 |
Success
44 | {successMessage}
45 |
46 |
47 |
48 | );
49 | }
50 | });
51 |
52 | SuccessModal = connect(
53 | mapStateToProps,
54 | mapDispatchToProps
55 | )(SuccessModal);
56 |
57 | export default SuccessModal;
58 |
--------------------------------------------------------------------------------
/src/components/stats/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import d3 from 'd3';
4 |
5 | import TasksSelectors from '../../stores/tasks_selectors';
6 | import StatsActionCreators from '../../stores/stats_action_creators';
7 | import StatsSelectors from '../../stores/stats_selectors';
8 |
9 | import StatsHeader from './stats_header';
10 | import StatsGraph from './stats_graph';
11 | import StatsSummary from './stats_summary';
12 |
13 | const mapStateToProps = (state) => ({
14 | currentTaskId: TasksSelectors.getCurrentTaskId(state),
15 | currentTask: TasksSelectors.getCurrentTask(state),
16 | currentTaskSummary: TasksSelectors.getCurrentTaskSummary(state),
17 | currentTaskExtent: TasksSelectors.getCurrentTaskExtent(state),
18 | statsFrom: StatsSelectors.getFromDate(state),
19 | statsTo: StatsSelectors.getToDate(state),
20 | statsByUser: StatsSelectors.getByUser(state),
21 | statsByDate: StatsSelectors.getByDate(state),
22 | });
23 |
24 | const mapDispatchToProps = {
25 | fetchAllStats: StatsActionCreators.fetchAllStats,
26 | };
27 |
28 | class Stats extends Component {
29 | fetchData() {
30 | const dateFormat = d3.time.format('%Y-%m-%d');
31 |
32 | const toDate = new Date();
33 | const fromDate = new Date(toDate.getFullYear() - 1, toDate.getMonth(), toDate.getDate());
34 |
35 | this.fetchStatsByRange(dateFormat(fromDate), dateFormat(toDate));
36 | }
37 |
38 | fetchStatsByRange = (fromDate, toDate) => {
39 | const { fetchAllStats, currentTaskId } = this.props;
40 | fetchAllStats({ taskId: currentTaskId, from: fromDate, to: toDate });
41 | }
42 |
43 | componentDidMount() {
44 | this.fetchData();
45 | }
46 |
47 | componentDidUpdate(prevProps) {
48 | // Refetch stats when a new task is selected
49 | if (prevProps.currentTaskId !== this.props.currentTaskId) {
50 | this.fetchData();
51 | }
52 | }
53 |
54 | render() {
55 | const {
56 | currentTask,
57 | statsFrom,
58 | statsTo,
59 | statsByUser,
60 | statsByDate,
61 | currentTaskSummary,
62 | currentTaskExtent,
63 | } = this.props;
64 |
65 | return (
66 |
67 |
68 |
72 |
77 |
79 |
80 |
81 | );
82 | }
83 | }
84 |
85 | Stats.propTypes = {
86 | currentTaskId: PropTypes.string.isRequired,
87 | currentTask: PropTypes.object.isRequired,
88 | currentTaskSummary: PropTypes.object.isRequired,
89 | currentTaskExtent: PropTypes.object.isRequired,
90 | statsFrom: PropTypes.string.isRequired,
91 | statsTo: PropTypes.string.isRequired,
92 | statsByUser: PropTypes.array.isRequired,
93 | statsByDate: PropTypes.array.isRequired,
94 | fetchAllStats: PropTypes.func.isRequired,
95 | };
96 |
97 | Stats = connect(
98 | mapStateToProps,
99 | mapDispatchToProps
100 | )(Stats);
101 |
102 | export default Stats;
103 |
--------------------------------------------------------------------------------
/src/components/stats/stats_graph.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 |
3 | import d3Graph from '../../utils/d3_graph';
4 |
5 | class StatsGraph extends Component {
6 | state = {
7 | // type filter = 'all' | 'edit' | 'fixed' | 'skip' | 'noterror'
8 | filter: 'all',
9 | }
10 |
11 | filterBy = (e) => {
12 | const filter = e.target.getAttribute('value');
13 | this.setState({ filter });
14 | }
15 |
16 | fetchIfChanged = (fromDate, toDate) => {
17 | const { statsFrom, statsTo, fetchStatsByRange } = this.props;
18 | if (fromDate !== statsFrom || toDate !== statsTo) {
19 | fetchStatsByRange(fromDate, toDate);
20 | }
21 | }
22 |
23 | getGraphState = () => {
24 | const { filter } = this.state;
25 | const { statsByDate } = this.props;
26 |
27 | const includedFields = (filter === 'all') ? ['edit', 'fixed', 'skip', 'noterror'] : [filter];
28 |
29 | const data = statsByDate
30 | .map(function(d) {
31 | d.value = includedFields.reduce((sum, field) => sum + d[field], 0);
32 | return d;
33 | })
34 | .sort(function(d1, d2) {
35 | return d1.start < d2.start;
36 | });
37 |
38 | return {
39 | data
40 | };
41 | }
42 |
43 | componentDidMount() {
44 | d3Graph.create(this.brushGraph);
45 | }
46 |
47 | componentDidUpdate() {
48 | d3Graph.update(this.brushGraph, this.getGraphState(), null, this.fetchIfChanged);
49 | }
50 |
51 | componentWillUnmount() {
52 | d3Graph.destroy(this.brushGraph);
53 | }
54 |
55 | render() {
56 | const { filter } = this.state;
57 |
58 | return (
59 |
78 | );
79 | }
80 | }
81 |
82 | StatsGraph.propTypes = {
83 | statsFrom: PropTypes.string.isRequired,
84 | statsTo: PropTypes.string.isRequired,
85 | statsByDate: PropTypes.array.isRequired,
86 | fetchStatsByRange: PropTypes.func.isRequired,
87 | };
88 |
89 | export default StatsGraph;
90 |
--------------------------------------------------------------------------------
/src/components/stats/stats_header.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import d3 from 'd3';
3 |
4 | const renderProgressBar = (taskSummary) => {
5 | const { items, fixed, noterror } = taskSummary;
6 |
7 | const total = items;
8 | const completed = fixed + noterror;
9 | const progressStyle = {
10 | width: (completed / total) * 100 + '%'
11 | };
12 |
13 | return (
14 |
15 |
16 | {d3.format(',')(completed)} complete
17 | of {d3.format(',')(total)}
18 |
19 |
22 |
23 | );
24 | };
25 |
26 | const StatsHeader = ({ task, taskSummary, statsFrom }) => {
27 | const taskName = task.value.name;
28 | const progressBar = renderProgressBar(taskSummary);
29 |
30 | return (
31 |
32 |
33 |
{taskName}
34 |
35 | {`Task last updated on ${statsFrom}.`}
36 |
37 |
38 | {progressBar}
39 |
40 | );
41 | };
42 |
43 | StatsHeader.propTypes = {
44 | task: PropTypes.object.isRequired,
45 | taskSummary: PropTypes.object.isRequired,
46 | statsFrom: PropTypes.string.isRequired,
47 | };
48 |
49 | export default StatsHeader;
50 |
--------------------------------------------------------------------------------
/src/components/stats/stats_summary.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import d3 from 'd3';
3 |
4 | import { USER_PROFILE_URL } from '../../config';
5 |
6 | const renderContributions = (statsByUser) => {
7 | if (statsByUser.length === 0) {
8 | return No data;
9 | }
10 |
11 | return statsByUser.map((stats, i) => {
12 | const { fixed, skip, noterror, edit, user } = stats;
13 |
14 | const profile = `${USER_PROFILE_URL}/${user}`;
15 | const numFormat = d3.format(',');
16 |
17 | const completed = fixed + noterror;
18 |
19 | return (
20 |
21 |
22 | {user}
23 |
24 |
{numFormat(fixed)}
25 |
{numFormat(skip)}
26 |
{numFormat(noterror)}
27 |
{numFormat(edit)}
28 |
{numFormat(completed)}
29 |
30 | );
31 | });
32 | };
33 |
34 | const StatsSummary = ({ statsByUser }) => {
35 | const contributions = renderContributions(statsByUser);
36 |
37 | return (
38 |
39 |
40 |
Contributors
41 | Fixed
42 | Skipped
43 | Not Error
44 | Edited
45 | Completed
46 |
47 | {contributions}
48 |
49 | );
50 | }
51 |
52 | StatsSummary.propTypes = {
53 | statsByUser: PropTypes.array.isRequired,
54 | };
55 |
56 | export default StatsSummary;
57 |
--------------------------------------------------------------------------------
/src/components/task/controls/task.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import ReactDOM from 'react-dom';
3 | import d3 from 'd3';
4 |
5 | const containerID = 'mapboxgl-ctrl-task';
6 |
7 | class TaskControl {
8 | onAdd(map) {
9 | this._map = map;
10 | this._container = this._createContainer();
11 | return this._container;
12 | }
13 |
14 | onRemove() {
15 | this._map = null;
16 | ReactDOM.unmountComponentAtNode(this._container);
17 | this._container.parentNode.removeChild(this._container);
18 | }
19 |
20 | update(task) {
21 | ReactDOM.render(
22 | ,
23 | document.getElementById(containerID)
24 | );
25 | }
26 |
27 | _createContainer() {
28 | const div = document.createElement('div');
29 | div.className = 'mapboxgl-ctrl';
30 | div.id = containerID;
31 | return div;
32 | }
33 | }
34 |
35 | class TaskComponent extends Component {
36 | state = {
37 | showMore: false,
38 | }
39 |
40 | toggleShowMore = () => {
41 | this.setState({ showMore: !this.state.showMore })
42 | }
43 |
44 | render() {
45 | const { task } = this.props;
46 | const { showMore } = this.state;
47 |
48 | const dateDisplay = d3.time.format('%B %-d');
49 | const timeDisplay = d3.time.format('%-I:%-M%p');
50 |
51 | const updatedDay = dateDisplay(new Date(task.value.updated * 1000));
52 | const updatedTime = timeDisplay(new Date(task.value.updated * 1000));
53 |
54 | const status = (task.isCompleted) ? 'Completed.' : 'Items remaining to be done.';
55 |
56 | const showMoreIcon = showMore ? 'icon caret-up' : 'icon caret-down';
57 | const showMoreClass = showMore ? '' : 'hidden';
58 |
59 | return (
60 |
61 |
62 |
63 |
64 |
65 | Title
66 | |
67 |
68 |
69 | {task.value.name}
70 |
71 |
72 | |
73 |
74 |
75 |
76 | Task ID
77 | |
78 |
79 | {task.idtask}
80 | |
81 |
82 |
83 |
84 | Description
85 | |
86 |
87 | {task.value.description}
88 | |
89 |
90 |
91 |
92 | Changeset comment
93 | |
94 |
95 | {task.value.changesetComment}
96 | |
97 |
98 |
99 |
100 | Updated
101 | |
102 |
103 | {updatedDay}
104 | {' '}
105 | {updatedTime}
106 | |
107 |
108 |
109 |
110 | Status
111 | |
112 |
113 | {status}
114 | |
115 |
116 |
117 |
118 |
119 | );
120 | }
121 | }
122 |
123 | export default TaskControl;
124 |
--------------------------------------------------------------------------------
/src/components/task/edit_bar.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 | import KeyBinding from 'react-keybinding';
3 | import { connect } from 'react-redux';
4 |
5 | import TasksSelectors from '../../stores/tasks_selectors';
6 | import UserSelectors from '../../stores/user_selectors';
7 | import SettingsSelectors from '../../stores/settings_selectors';
8 | import ItemsSelectors from '../../stores/items_selectors';
9 | import ItemsActionCreators from '../../stores/items_action_creators';
10 | import ModalsActionCreators from '../../stores/modals_action_creators';
11 |
12 | const mapStateToProps = (state) => ({
13 | currentTaskId: TasksSelectors.getCurrentTaskId(state),
14 | currentTask: TasksSelectors.getCurrentTask(state),
15 | currentTaskType: TasksSelectors.getCurrentTaskType(state),
16 | user: UserSelectors.getUsername(state),
17 | isAuthenticated: UserSelectors.getIsAuthenticated(state),
18 | editor: SettingsSelectors.getEditorSetting(state),
19 | currentItemId: ItemsSelectors.getCurrentItemId(state),
20 | });
21 |
22 | const mapDispatchToProps = {
23 | updateItem: ItemsActionCreators.updateItem,
24 | openSettingsModal: ModalsActionCreators.openSettingsModal,
25 | };
26 |
27 | let EditBar = React.createClass({
28 | propTypes: {
29 | currentTaskId: PropTypes.string.isRequired,
30 | currentTask: PropTypes.object.isRequired,
31 | currentTaskType: PropTypes.string.isRequired,
32 | user: PropTypes.string,
33 | isAuthenticated: PropTypes.bool.isRequired,
34 | editor: PropTypes.string.isRequired,
35 | currentItemId: PropTypes.string.isRequired,
36 | updateItem: PropTypes.func.isRequired,
37 | onTaskEdit: PropTypes.func.isRequired,
38 | onUpdate: PropTypes.func.isRequired,
39 | openSettingsModal: PropTypes.func.isRequired,
40 | },
41 |
42 | mixins: [KeyBinding],
43 |
44 | keybindings: {
45 | 'e': function(e) {
46 | if (this.props.isAuthenticated) this.edit()
47 | },
48 | 's': function(e) {
49 | if (this.props.isAuthenticated) this.skip()
50 | },
51 | 'f': function(e) {
52 | if (this.props.isAuthenticated) this.fixed()
53 | },
54 | 'n': function(e) {
55 | if (this.props.isAuthenticated) this.noterror()
56 | },
57 | },
58 |
59 | updateItem(action) {
60 | const { updateItem, user, editor, currentTaskId, currentTaskType, currentItemId, onUpdate } = this.props;
61 | const payload = {
62 | user,
63 | editor,
64 | action,
65 | key: currentItemId,
66 | };
67 | updateItem({ taskId: currentTaskId, taskType: currentTaskType, payload }).then(onUpdate);
68 | },
69 |
70 | edit() { this.props.onTaskEdit() },
71 | skip() { this.updateItem('skip') },
72 | fixed() { this.updateItem('fixed') },
73 | noterror() { this.updateItem('noterror') },
74 |
75 | render() {
76 | const { currentTask, isAuthenticated, editor, geolocation, onUpdate, onTaskEdit, openSettingsModal } = this.props;
77 |
78 | const taskTitle = currentTask.value.name;
79 | let taskActions = (
80 |
83 | );
84 |
85 | if (isAuthenticated) {
86 | const editorName = editor === 'josm' ? 'JOSM' : 'iD';
87 |
88 | taskActions = (
89 |
107 | );
108 | }
109 |
110 | return (
111 |
112 |
113 | {taskActions}
114 |
115 | {taskTitle}
116 | {geolocation && {geolocation}}
117 |
118 |
119 |
120 | );
121 | }
122 | });
123 |
124 | EditBar = connect(
125 | mapStateToProps,
126 | mapDispatchToProps,
127 | )(EditBar);
128 |
129 | export default EditBar;
130 |
--------------------------------------------------------------------------------
/src/components/task/index.js:
--------------------------------------------------------------------------------
1 | import React, { Component, PropTypes } from 'react';
2 | import { connect } from 'react-redux';
3 | import { featureCollection } from '@turf/helpers';
4 | import turfCentroid from '@turf/centroid';
5 | import turfBbox from '@turf/bbox';
6 | import turfBboxPolygon from '@turf/bbox-polygon';
7 |
8 | import mapboxgl from 'mapbox-gl/dist/mapbox-gl';
9 | import 'mapbox-gl/dist/mapbox-gl.css';
10 |
11 | import EditBar from './edit_bar';
12 |
13 | import { MAPBOX_ACCESS_TOKEN, MAPBOX_GEOCODER_URL, JOSM_RC_URL, ID_URL } from '../../config';
14 | import ItemsActionCreators from '../../stores/items_action_creators';
15 | import ModalsActionCreators from '../../stores/modals_action_creators';
16 | import ItemsSelectors from '../../stores/items_selectors';
17 | import UserSelectors from '../../stores/user_selectors';
18 | import SettingsSelectors from '../../stores/settings_selectors';
19 | import TasksSelectors from '../../stores/tasks_selectors';
20 |
21 | import TaskControl from './controls/task';
22 | const taskControl = new TaskControl();
23 |
24 | const mapStateToProps = (state) => ({
25 | currentTask: TasksSelectors.getCurrentTask(state),
26 | currentTaskId: TasksSelectors.getCurrentTaskId(state),
27 | currentTaskType: TasksSelectors.getCurrentTaskType(state),
28 | user: UserSelectors.getUsername(state),
29 | editor: SettingsSelectors.getEditorSetting(state),
30 | sidebar: SettingsSelectors.getSidebarSetting(state),
31 | currentItem: ItemsSelectors.getCurrentItem(state),
32 | currentItemId: ItemsSelectors.getCurrentItemId(state),
33 | });
34 |
35 | const mapDispatchToProps = {
36 | fetchRandomItem: ItemsActionCreators.fetchRandomItem,
37 | openErrorModal: ModalsActionCreators.openErrorModal,
38 | };
39 |
40 | mapboxgl.accessToken = MAPBOX_ACCESS_TOKEN;
41 |
42 | class Task extends Component {
43 | state = {
44 | map: null,
45 | geolocation: '',
46 | }
47 |
48 | editTask = () => {
49 | const { map } = this.state;
50 | const { editor } = this.props;
51 |
52 | if (editor === 'josm') {
53 | const { currentItem } = this.props;
54 | const { _osmType, _osmId } = currentItem.properties;
55 |
56 | if (_osmType && _osmId) {
57 | const objects = `${_osmType.substr(0, 1)}${_osmId}`;
58 |
59 | const query = {
60 | new_layer: true,
61 | objects,
62 | relation_members: true,
63 | };
64 | const params = Object.keys(query).map(key => `${key}=${query[key]}`).join('&');
65 |
66 | fetch(`${JOSM_RC_URL}/load_object?${params}`)
67 | .catch(() => this.props.openErrorModal('Could not connect to JOSM remote control.'));
68 | } else {
69 | const bounds = map.getBounds();
70 |
71 | const bottom = bounds.getSouthWest().lat - 0.0005;
72 | const left = bounds.getSouthWest().lng - 0.0005;
73 | const top = bounds.getNorthEast().lat - 0.0005;
74 | const right = bounds.getNorthEast().lng - 0.0005;
75 |
76 | const query = { left, right, top, bottom };
77 | const params = Object.keys(query).map(key => `${key}=${query[key]}`).join('&');
78 |
79 | fetch(`${JOSM_RC_URL}/load_and_zoom?${params}`)
80 | .catch(() => this.props.openErrorModal('Could not connect to JOSM remote control.'));
81 | }
82 | }
83 |
84 | if (editor === 'id') {
85 | const {lng, lat} = map.getCenter();
86 | const zoom = map.getZoom();
87 | const { currentTask } = this.props;
88 | const { changesetComment } = currentTask.value;
89 |
90 | const iDEditPath = `${ID_URL}/#comment=${encodeURI(changesetComment)}&map=${zoom}/${lat}/${lng}`;
91 |
92 | window.open(iDEditPath, '_blank');
93 | }
94 | }
95 |
96 | reset = () => {
97 | this.setState({
98 | geolocation: '',
99 | });
100 | }
101 |
102 | fetchNextItem = () => {
103 | const { user, editor, currentTaskId, currentTaskType, fetchRandomItem } = this.props;
104 | const payload = {
105 | user: user || '',
106 | editor,
107 | };
108 |
109 | fetchRandomItem({ taskId: currentTaskId, taskType: currentTaskType, payload })
110 | .then(this.reset);
111 | }
112 |
113 | geolocate = (center) => {
114 | const [lng, lat] = center;
115 | const placeRegex = /place./;
116 |
117 | fetch(`${MAPBOX_GEOCODER_URL}/mapbox.places/${lng},${lat}.json?types=place&access_token=${MAPBOX_ACCESS_TOKEN}`)
118 | .then(data => data.json())
119 | .then(json => (json.features.length && json.features.find(f => placeRegex.test(f.id)).place_name) || '')
120 | .then(geolocation => this.setState({ geolocation }));
121 | }
122 |
123 | addSource(id) {
124 | const { map } = this.state;
125 | map.addSource(id, {
126 | type: 'geojson',
127 | data: featureCollection([]),
128 | });
129 | map.addSource(`${id}-bbox`, {
130 | type: 'geojson',
131 | data: featureCollection([]),
132 | });
133 | }
134 |
135 | updateSource(id, feature) {
136 | const { map } = this.state;
137 | const flattenedFeatures = this.flattenRelations(feature);
138 | const geojson = featureCollection(flattenedFeatures);
139 | const bbox = this.getBoundingBox(geojson);
140 | map.getSource(id).setData(geojson);
141 | map.getSource(`${id}-bbox`).setData(bbox);
142 | }
143 |
144 | removeSource(id) {
145 | const { map } = this.state;
146 | map.removeSource(id);
147 | map.removeSource(`${id}-bbox`);
148 | }
149 |
150 | flattenRelations(feature) {
151 | const relations = feature.properties.relations || [];
152 | return [feature].concat(relations);
153 | }
154 |
155 | getBoundingBox(geojson) {
156 | const [ minX, minY, maxX, maxY ] = turfBbox(geojson);
157 | const padX = Math.max((maxX - minX) / 5, 0.0001);
158 | const padY = Math.max((maxY - minY) / 5, 0.0001);
159 | const bboxWithPadding = [
160 | minX - padX,
161 | minY - padY,
162 | maxX + padX,
163 | maxY + padY,
164 | ];
165 | const bboxPolygon = turfBboxPolygon(bboxWithPadding);
166 | return featureCollection([bboxPolygon]);
167 | }
168 |
169 | addLayers(id) {
170 | const { map } = this.state;
171 |
172 | map.addLayer({
173 | id: `${id}-bbox-bg`,
174 | type: 'line',
175 | source: `${id}-bbox`,
176 | paint: {
177 | 'line-width': 6,
178 | 'line-color': 'HSL(247, 100%, 100%)',
179 | 'line-opacity': 0.5,
180 | },
181 | });
182 |
183 | map.addLayer({
184 | id: `${id}-bbox`,
185 | type: 'line',
186 | source: `${id}-bbox`,
187 | paint: {
188 | 'line-width': 2,
189 | 'line-color': 'HSL(247, 60%, 50%)',
190 | 'line-opacity': 0.75,
191 | },
192 | });
193 |
194 | map.addLayer({
195 | id: `${id}-circle`,
196 | type: 'circle',
197 | source: id,
198 | paint: {
199 | 'circle-radius': {
200 | 'stops': [
201 | [12,4],
202 | [16,8]
203 | ]
204 | },
205 | 'circle-opacity': {
206 | 'stops': [
207 | [12,0.5],
208 | [16,0.75]
209 | ]
210 | },
211 | 'circle-color': '#dc322f',
212 | },
213 | });
214 |
215 | map.addLayer({
216 | id: `${id}-line`,
217 | type: 'line',
218 | source: id,
219 | paint: {
220 | 'line-width': {
221 | 'stops': [
222 | [12,2],
223 | [16,5]
224 | ]
225 | },
226 | 'line-opacity': 0.75,
227 | 'line-color': '#dc322f',
228 | },
229 | });
230 |
231 | map.addLayer({
232 | id: `${id}-fill`,
233 | type: 'fill',
234 | source: id,
235 | paint: {
236 | 'fill-opacity': 0,
237 | 'fill-outline-color': '#dc322f',
238 | },
239 | });
240 | }
241 |
242 | removeLayers(id) {
243 | const { map } = this.state;
244 | map.removeLayer(`${id}-circle`);
245 | map.removeLayer(`${id}-line`);
246 | map.removeLayer(`${id}-fill`);
247 | map.removeLayer(`${id}-bbox`);
248 | }
249 |
250 | fitBounds(bounds) {
251 | const { map } = this.state;
252 | map.fitBounds(bounds, { linear: true, padding: 200 });
253 | }
254 |
255 | resize() {
256 | const { map } = this.state;
257 | // Needs a slight delay because of the sidebar animation
258 | window.setTimeout(() => map.resize(), 120);
259 | }
260 |
261 | componentDidMount() {
262 | const map = new mapboxgl.Map({
263 | container: this.mapContainer,
264 | style: 'mapbox://styles/mapbox/streets-v9',
265 | zoom: 18,
266 | center: [-74.50, 40],
267 | });
268 |
269 | map.addControl(new mapboxgl.NavigationControl());
270 | map.addControl(taskControl, 'top-left');
271 |
272 | map.once('load', () => {
273 | this.setState({ map })
274 | taskControl.update(this.props.currentTask);
275 | });
276 | }
277 |
278 | shouldComponentUpdate(nextProps, nextState) {
279 | return (
280 | nextState.map !== this.state.map ||
281 | nextState.geolocation !== this.state.geolocation ||
282 | nextProps.currentTaskId !== this.props.currentTaskId ||
283 | nextProps.currentItemId !== this.props.currentItemId ||
284 | nextProps.sidebar !== this.props.sidebar
285 | );
286 | }
287 |
288 | componentDidUpdate(prevProps, prevState) {
289 | if (prevState.map !== this.state.map) {
290 | const { currentTaskId } = this.props;
291 | this.addSource(currentTaskId);
292 | this.addLayers(currentTaskId);
293 | this.fetchNextItem();
294 | return;
295 | }
296 |
297 | if (this.state.map && prevProps.currentTaskId !== this.props.currentTaskId) {
298 | this.removeSource(prevProps.currentTaskId);
299 | this.removeLayers(prevProps.currentTaskId);
300 | this.addSource(this.props.currentTaskId);
301 | this.addLayers(this.props.currentTaskId);
302 | this.fetchNextItem();
303 | taskControl.update(this.props.currentTask);
304 | return;
305 | }
306 |
307 | if (this.state.map && prevProps.currentItemId !== this.props.currentItemId) {
308 | const { currentTaskId, currentItem } = this.props;
309 | this.updateSource(currentTaskId, currentItem);
310 |
311 | const center = turfCentroid(currentItem).geometry.coordinates;
312 | this.geolocate(center);
313 |
314 | const bboxPolygon = this.state.map.getSource(`${currentTaskId}-bbox`)._data.features[0];
315 | const bboxCoordiantes = bboxPolygon.geometry.coordinates[0];
316 | this.fitBounds([bboxCoordiantes[0], bboxCoordiantes[2]]);
317 | return;
318 | }
319 |
320 | if (this.state.map && prevProps.sidebar !== this.props.sidebar) {
321 | this.resize();
322 | }
323 | }
324 |
325 | componentWillUnmount() {
326 | const { map } = this.state;
327 | if (map) map.remove();
328 | }
329 |
330 | render() {
331 | const { geolocation, iDEdit, iDEditPath } = this.state;
332 | const { currentItemId } = this.props;
333 |
334 | const editBar = (
335 |
339 | );
340 |
341 | return (
342 | this.mapContainer = node} className='mode active map fill-navy-dark contain'>
343 | { currentItemId && editBar }
344 |
345 | );
346 | }
347 | }
348 |
349 | Task.propTypes = {
350 | currentTask: PropTypes.object.isRequired,
351 | currentTaskId: PropTypes.string.isRequired,
352 | user: PropTypes.string,
353 | editor: PropTypes.string.isRequired,
354 | sidebar: PropTypes.bool.isRequired,
355 | currentItem: PropTypes.object,
356 | currentItemId: PropTypes.string,
357 | fetchRandomItem: PropTypes.func.isRequired,
358 | openErrorModal: PropTypes.func.isRequired,
359 | };
360 |
361 | Task = connect(
362 | mapStateToProps,
363 | mapDispatchToProps
364 | )(Task);
365 |
366 | export default Task;
367 |
--------------------------------------------------------------------------------
/src/config/config_development.js:
--------------------------------------------------------------------------------
1 | export const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiYWppdGhyYW5rYSIsImEiOiJjaXA5b20zdXQwMW9xdGNseXJ6M2JpeHZlIn0.FTSj2x9tyD37djfYYWrtAw';
2 |
3 | export const TASK_SERVER_URL = 'https://build-to-fix-staging.tilestream.net';
4 | export const USER_PROFILE_URL = 'https://www.openstreetmap.org/user';
5 | export const MAPBOX_GEOCODER_URL = 'https://api.mapbox.com/geocoding/v5';
6 | export const JOSM_RC_URL = 'http://localhost:8111';
7 | export const ID_URL = 'http://preview.ideditor.com/release';
8 |
--------------------------------------------------------------------------------
/src/config/config_production.js:
--------------------------------------------------------------------------------
1 | export const MAPBOX_ACCESS_TOKEN = 'pk.eyJ1IjoiYWppdGhyYW5rYSIsImEiOiJjaXA5b20zdXQwMW9xdGNseXJ6M2JpeHZlIn0.FTSj2x9tyD37djfYYWrtAw';
2 |
3 | export const TASK_SERVER_URL = 'https://build-to-fix-production.mapbox.com';
4 | export const USER_PROFILE_URL = 'https://www.openstreetmap.org/user';
5 | export const MAPBOX_GEOCODER_URL = 'https://api.mapbox.com/geocoding/v5';
6 | export const JOSM_RC_URL = 'http://localhost:8111';
7 | export const ID_URL = 'http://preview.ideditor.com/release';
8 |
--------------------------------------------------------------------------------
/src/config/index.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 | module.exports = require('./config_production');
3 | } else {
4 | module.exports = require('./config_development');
5 | }
6 |
--------------------------------------------------------------------------------
/src/constants/activity_constants.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 |
3 | const ActivityConstants = keymirror({
4 | ACTIVITY_FETCH_RECENT: null,
5 | });
6 |
7 | export default ActivityConstants;
8 |
--------------------------------------------------------------------------------
/src/constants/admin_constants.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 |
3 | const AdminConstants = keymirror({
4 | ADMIN_FETCH_ALL_USERS: null,
5 | ADMIN_CHANGE_USER_ROLE: null,
6 | });
7 |
8 | export default AdminConstants;
9 |
--------------------------------------------------------------------------------
/src/constants/items_constants.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 |
3 | const ItemsConstants = keymirror({
4 | ITEMS_FETCH_RANDOM: null,
5 | ITEMS_UPDATE: null,
6 | });
7 |
8 | export default ItemsConstants;
9 |
--------------------------------------------------------------------------------
/src/constants/loading_constants.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 |
3 | const LoadingConstants = keymirror({
4 | LOADING_START: null,
5 | LOADING_STOP: null,
6 | });
7 |
8 | export default LoadingConstants;
9 |
--------------------------------------------------------------------------------
/src/constants/modals_constants.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 |
3 | const ModalsConstants = keymirror({
4 | MODALS_OPEN_SETTINGS: null,
5 | MODALS_CLOSE_SETTINGS: null,
6 | MODALS_OPEN_SUCCESS: null,
7 | MODALS_CLOSE_SUCCESS: null,
8 | MODALS_OPEN_ERROR: null,
9 | MODALS_CLOSE_ERROR: null,
10 | MODALS_OPEN_CREATE_TASK: null,
11 | MODALS_CLOSE_CREATE_TASK: null,
12 | MODALS_OPEN_MANAGE_USERS: null,
13 | MODALS_CLOSE_MANAGE_USERS: null,
14 | });
15 |
16 | export default ModalsConstants;
17 |
--------------------------------------------------------------------------------
/src/constants/settings_constants.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 |
3 | const SettingsConstants = keymirror({
4 | SETTINGS_TOGGLE_SIDEBAR: null,
5 | SETTINGS_SET_EDITOR_PREFERENCE: null,
6 | });
7 |
8 | export default SettingsConstants;
9 |
--------------------------------------------------------------------------------
/src/constants/stats_constants.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 |
3 | const StatsConstants = keymirror({
4 | STATS_FETCH_ALL: null,
5 | });
6 |
7 | export default StatsConstants;
8 |
--------------------------------------------------------------------------------
/src/constants/tasks_constants.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 |
3 | const TasksConstants = keymirror({
4 | TASKS_FETCH_ALL: null,
5 | TASKS_FETCH_BY_ID: null,
6 | TASKS_SELECT: null,
7 | TASKS_CREATE: null,
8 | TASKS_UPDATE: null,
9 | TASKS_DESTROY: null,
10 | });
11 |
12 | export default TasksConstants;
13 |
--------------------------------------------------------------------------------
/src/constants/user_constants.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 |
3 | const UserConstants = keymirror({
4 | USER_LOGGED_IN: null,
5 | USER_FETCH_USER_DETAILS: null,
6 | USER_LOGOUT: null,
7 | });
8 |
9 | export default UserConstants;
10 |
11 | export const ROLES = {
12 | EDITOR: 'editor',
13 | ADMIN: 'admin',
14 | SUPERADMIN: 'superadmin',
15 | };
16 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // Polyfills
2 | import 'whatwg-fetch';
3 |
4 | import React from 'react';
5 | import { render } from 'react-dom';
6 | import { Provider } from 'react-redux';
7 | import { Router, hashHistory } from 'react-router';
8 |
9 | import routes from './routes';
10 | import store from './stores/store';
11 |
12 | render((
13 |
14 |
18 |
19 | ), document.getElementById('app'));
20 |
--------------------------------------------------------------------------------
/src/routes.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Route } from 'react-router';
3 |
4 | import App from './components/app';
5 | import Task from './components/task';
6 | import Stats from './components/stats';
7 | import Admin from './components/admin';
8 | import Activity from './components/activity';
9 |
10 | import store from './stores/store';
11 | import TasksActionCreators from './stores/tasks_action_creators';
12 |
13 | const onEnter = (nextState) => {
14 | const nextTaskId = nextState.params.task;
15 | if (nextTaskId) {
16 | store.dispatch(TasksActionCreators.selectTask({ idtask: nextTaskId }));
17 | }
18 | };
19 |
20 | const onChange = (prevState, nextState) => {
21 | const prevTaskId = prevState.params.task;
22 | const nextTaskId = nextState.params.task;
23 |
24 | if (prevTaskId !== nextTaskId) {
25 | store.dispatch(TasksActionCreators.selectTask({ idtask: nextTaskId }));
26 | }
27 | };
28 |
29 | export default (
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 |
--------------------------------------------------------------------------------
/src/services/index.js:
--------------------------------------------------------------------------------
1 | import * as server from './server';
2 |
3 | export {
4 | server,
5 | };
6 |
--------------------------------------------------------------------------------
/src/services/server.js:
--------------------------------------------------------------------------------
1 | import { TASK_SERVER_URL as baseURL } from '../config';
2 |
3 | const toJSON = response => response.json();
4 |
5 | // Status
6 | export const status = () => (
7 | fetch(`${baseURL}`)
8 | .then(toJSON)
9 | );
10 |
11 | // User
12 | export const loginURL =
13 | `${baseURL}/connect/openstreetmap`;
14 |
15 | export const fetchUserDetails = ({ token }) => (
16 | fetch(`${baseURL}/user/details`, {
17 | headers: {
18 | 'Authorization': token,
19 | },
20 | })
21 | .then(toJSON)
22 | );
23 |
24 | export const logout = ({ token }) => (
25 | fetch(`${baseURL}/logout`, {
26 | headers: {
27 | 'Authorization': token,
28 | },
29 | })
30 | .then(toJSON)
31 | );
32 |
33 | // Admin
34 | export const fetchAllUsers = ({ token }) => (
35 | fetch(`${baseURL}/users`, {
36 | headers: {
37 | 'Authorization': token,
38 | },
39 | })
40 | .then(toJSON)
41 | );
42 |
43 | export const changeUserRole = ({ token, payload }) => (
44 | fetch(`${baseURL}/users`, {
45 | method: 'PUT',
46 | headers: {
47 | 'Authorization': token,
48 | },
49 | body: JSON.stringify(payload),
50 | })
51 | .then(toJSON)
52 | );
53 |
54 | export const destroyUser = ({ token, payload }) => (
55 | fetch(`${baseURL}/users`, {
56 | method: 'DELETE',
57 | headers: {
58 | 'Authorization': token,
59 | },
60 | body: JSON.stringify(payload),
61 | })
62 | .then(toJSON)
63 | );
64 |
65 | // Tasks
66 | export const fetchAllTasks = () => (
67 | fetch(`${baseURL}/tasks`)
68 | .then(toJSON)
69 | );
70 |
71 | export const fetchTaskById = ({ taskId }) => (
72 | fetch(`${baseURL}/tasks/${taskId}`)
73 | .then(toJSON)
74 | );
75 |
76 | export const createTask = ({ token, payload }) => (
77 | fetch(`${baseURL}/tasks`, {
78 | method: 'POST',
79 | headers: {
80 | 'Authorization': token,
81 | },
82 | body: payload,
83 | })
84 | .then(toJSON)
85 | );
86 |
87 | export const updateTask = ({ token, payload }) => (
88 | fetch(`${baseURL}/tasks`, {
89 | method: 'PUT',
90 | headers: {
91 | 'Authorization': token,
92 | },
93 | body: payload,
94 | })
95 | .then(toJSON)
96 | );
97 |
98 | export const destroyTask = ({ token, payload }) => (
99 | fetch(`${baseURL}/tasks`, {
100 | method: 'DELETE',
101 | headers: {
102 | 'Authorization': token,
103 | },
104 | body: payload,
105 | })
106 | .then(toJSON)
107 | );
108 |
109 | // Items
110 | export const fetchRandomItem = ({ taskId, taskType, payload }) => (
111 | fetch(`${baseURL}/tasks/${taskId}/${taskType}/items`, {
112 | method: 'POST',
113 | body: JSON.stringify(payload),
114 | })
115 | .then(toJSON)
116 | );
117 |
118 | export const updateItem = ({ taskId, taskType, payload }) => (
119 | fetch(`${baseURL}/tasks/${taskId}/${taskType}/items`, {
120 | method: 'PUT',
121 | body: JSON.stringify(payload),
122 | })
123 | .then(toJSON)
124 | );
125 |
126 | // Activity
127 | export const fetchRecentActivity = ({ taskId }) => (
128 | fetch(`${baseURL}/tasks/${taskId}/activity`)
129 | .then(toJSON)
130 | );
131 |
132 | // Stats
133 | export const fetchAllStats = ({ taskId, from, to }) => (
134 | fetch(`${baseURL}/tasks/${taskId}/track_stats/from:${from}/to:${to}`)
135 | .then(toJSON)
136 | );
137 |
--------------------------------------------------------------------------------
/src/stores/activity_action_creators.js:
--------------------------------------------------------------------------------
1 | import { server } from '../services';
2 | import { asyncAction } from './async_action';
3 | import ActivityConstants from '../constants/activity_constants';
4 |
5 | const ActivityActionCreators = {
6 | fetchRecentActivity: asyncAction({
7 | type: ActivityConstants.ACTIVITY_FETCH_RECENT,
8 | asyncCall: server.fetchRecentActivity,
9 | showLoader: true,
10 | }),
11 | };
12 |
13 | export default ActivityActionCreators;
14 |
--------------------------------------------------------------------------------
/src/stores/activity_reducer.js:
--------------------------------------------------------------------------------
1 | import ActivityConstants from '../constants/activity_constants';
2 | import TasksConstants from '../constants/tasks_constants';
3 | import { AsyncStatus } from './async_action';
4 |
5 | const initialState = {
6 | data: [],
7 | fromDate: '',
8 | toDate: '',
9 | updatedOn: null,
10 | };
11 |
12 | const activity = (state = initialState, action) => {
13 | // Reset state when a new task is selected
14 | if (action.type === TasksConstants.TASKS_SELECT) {
15 | return initialState;
16 | }
17 |
18 | switch(action.type) {
19 | case ActivityConstants.ACTIVITY_FETCH_RECENT:
20 | if (action.status === AsyncStatus.SUCCESS) {
21 | const { data, updated } = action.response;
22 | const { from, to } = action.params;
23 | return {
24 | data,
25 | fromDate: from,
26 | toDate: to,
27 | updatedOn: updated,
28 | };
29 | }
30 | return state;
31 | default:
32 | return state;
33 | }
34 | };
35 |
36 | export default activity;
37 |
--------------------------------------------------------------------------------
/src/stores/activity_selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const activitySelector = state => state.activity;
4 |
5 | const ActivitySelectors = {
6 | getData: createSelector(activitySelector, state => state.data.sort((a, b) => b.time - a.time)),
7 | getFromDate: createSelector(activitySelector, state => state.fromDate),
8 | getToDate: createSelector(activitySelector, state => state.toDate),
9 | getUpdatedOn: createSelector(activitySelector, state => state.updatedOn),
10 | };
11 |
12 | export default ActivitySelectors;
13 |
--------------------------------------------------------------------------------
/src/stores/admin_action_creators.js:
--------------------------------------------------------------------------------
1 | import { server } from '../services';
2 | import { asyncAction } from './async_action';
3 | import AdminConstants from '../constants/admin_constants';
4 |
5 | const AdminActionCreators = {
6 | fetchAllUsers: asyncAction({
7 | type: AdminConstants.ADMIN_FETCH_ALL_USERS,
8 | asyncCall: server.fetchAllUsers,
9 | showLoader: true,
10 | }),
11 |
12 | changeUserRole: asyncAction({
13 | type: AdminConstants.ADMIN_CHANGE_USER_ROLE,
14 | asyncCall: server.changeUserRole,
15 | showLoader: true,
16 | showError: false,
17 | }),
18 | };
19 |
20 | export default AdminActionCreators;
21 |
--------------------------------------------------------------------------------
/src/stores/admin_reducer.js:
--------------------------------------------------------------------------------
1 | import AdminConstants from '../constants/admin_constants';
2 | import UserConstants from '../constants/user_constants';
3 | import { AsyncStatus } from './async_action';
4 |
5 | const initialState = {
6 | users: [],
7 | };
8 |
9 | const admin = (state = initialState, action) => {
10 | switch(action.type) {
11 | case AdminConstants.ADMIN_FETCH_ALL_USERS:
12 | if (action.status === AsyncStatus.SUCCESS) {
13 | return {
14 | users: action.response.users,
15 | };
16 | }
17 | return state;
18 | case AdminConstants.ADMIN_CHANGE_USER_ROLE:
19 | if (action.status === AsyncStatus.SUCCESS) {
20 | const { payload: { iduser, role } } = action.params;
21 | const users = state.users.map(u => {
22 | if (u.id == iduser) {
23 | u.role = role;
24 | }
25 | return u;
26 | });
27 | return {
28 | users,
29 | };
30 | }
31 | return state;
32 | case UserConstants.USER_LOGOUT:
33 | return {
34 | users: [],
35 | };
36 | default:
37 | return state;
38 | }
39 | };
40 |
41 | export default admin;
42 |
--------------------------------------------------------------------------------
/src/stores/admin_selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const adminSelector = state => state.admin;
4 |
5 | const sortByName = (users) => {
6 | return users.sort((a, b) => {
7 | if (a.user.toLowerCase() < b.user.toLowerCase()) return -1;
8 | if (a.user.toLowerCase() > b.user.toLowerCase()) return 1;
9 | return 0;
10 | });
11 | };
12 |
13 | const AdminSelectors = {
14 | getUsers: createSelector(adminSelector, state => sortByName(state.users)),
15 | };
16 |
17 | export default AdminSelectors;
18 |
--------------------------------------------------------------------------------
/src/stores/async_action.js:
--------------------------------------------------------------------------------
1 | import keymirror from 'keymirror';
2 | import { normalize } from 'normalizr';
3 |
4 | import LoadingActionCreators from './loading_action_creators';
5 | import ModalsActionCreators from './modals_action_creators';
6 |
7 | export const AsyncStatus = keymirror({
8 | REQUEST: null,
9 | SUCCESS: null,
10 | FAILURE: null,
11 | });
12 |
13 | export const checkStatusCode = (response) => {
14 | const { statusCode } = response;
15 | if (statusCode && statusCode >= 400) {
16 | return Promise.reject(response.message || 'Something went wrong.');
17 | }
18 | return response;
19 | };
20 |
21 | export const asyncAction = ({ type, asyncCall, responseSchema, showLoader = false, showError=true }) => {
22 | return (params = {}) => (dispatch) => {
23 | dispatch({
24 | type,
25 | status: AsyncStatus.REQUEST,
26 | ...params,
27 | });
28 |
29 | if (showLoader) dispatch(LoadingActionCreators.startLoading());
30 |
31 | return asyncCall(params)
32 | .then(checkStatusCode)
33 | .then(
34 | response => {
35 | if (showLoader) dispatch(LoadingActionCreators.stopLoading());
36 |
37 | return dispatch({
38 | type,
39 | status: AsyncStatus.SUCCESS,
40 | response: responseSchema ? normalize(response, responseSchema) : response,
41 | params,
42 | });
43 | },
44 | error => {
45 | if (showLoader) dispatch(LoadingActionCreators.stopLoading());
46 | if (showError) dispatch(ModalsActionCreators.openErrorModal(error));
47 |
48 | return dispatch({
49 | type,
50 | status: AsyncStatus.FAILURE,
51 | error,
52 | });
53 | }
54 | );
55 | };
56 | };
57 |
--------------------------------------------------------------------------------
/src/stores/items_action_creators.js:
--------------------------------------------------------------------------------
1 | import { server } from '../services';
2 | import schemas from './schemas';
3 | import { asyncAction } from './async_action';
4 | import ItemsConstants from '../constants/items_constants';
5 |
6 | const ItemsActionCreators = {
7 | fetchRandomItem: asyncAction({
8 | type: ItemsConstants.ITEMS_FETCH_RANDOM,
9 | asyncCall: server.fetchRandomItem,
10 | responseSchema: schemas.item,
11 | showLoader: true,
12 | }),
13 |
14 | updateItem: asyncAction({
15 | type: ItemsConstants.ITEMS_UPDATE,
16 | asyncCall: server.updateItem,
17 | showLoader: true,
18 | }),
19 | };
20 |
21 | export default ItemsActionCreators;
22 |
--------------------------------------------------------------------------------
/src/stores/items_reducer.js:
--------------------------------------------------------------------------------
1 | import ItemsConstants from '../constants/items_constants';
2 | import TasksConstants from '../constants/tasks_constants';
3 | import { AsyncStatus } from './async_action';
4 |
5 | const initialState = {
6 | item: {},
7 | itemId: null,
8 | };
9 |
10 | const items = (state = initialState, action) => {
11 | // Reset state when a new task is selected
12 | if (action.type === TasksConstants.TASKS_SELECT) {
13 | return initialState;
14 | }
15 |
16 | switch(action.type) {
17 | case ItemsConstants.ITEMS_FETCH_RANDOM:
18 | if (action.status === AsyncStatus.SUCCESS) {
19 | const { entities: { items }, result } = action.response;
20 | return {
21 | item: items[result],
22 | itemId: result,
23 | };
24 | }
25 | return state;
26 | default:
27 | return state;
28 | }
29 | };
30 |
31 | export default items;
32 |
--------------------------------------------------------------------------------
/src/stores/items_selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const itemsSelector = state => state.items;
4 |
5 | const ItemsSelectors = {
6 | getCurrentItem: createSelector(itemsSelector, (state) => state.item),
7 | getCurrentItemId: createSelector(itemsSelector, (state) => state.itemId),
8 | };
9 |
10 | export default ItemsSelectors;
11 |
--------------------------------------------------------------------------------
/src/stores/loading_action_creators.js:
--------------------------------------------------------------------------------
1 | import LoadingConstants from '../constants/loading_constants';
2 |
3 | const LoadingActionCreators = {
4 | startLoading: () => ({
5 | type: LoadingConstants.LOADING_START,
6 | }),
7 |
8 | stopLoading: () => ({
9 | type: LoadingConstants.LOADING_STOP,
10 | }),
11 | };
12 |
13 | export default LoadingActionCreators;
14 |
--------------------------------------------------------------------------------
/src/stores/loading_reducer.js:
--------------------------------------------------------------------------------
1 | import LoadingConstants from '../constants/loading_constants';
2 |
3 | const initialState = false;
4 |
5 | const loading = (state = initialState, action) => {
6 | switch(action.type) {
7 | case LoadingConstants.LOADING_START:
8 | return true;
9 | case LoadingConstants.LOADING_STOP:
10 | return false;
11 | default:
12 | return state;
13 | }
14 | }
15 |
16 | export default loading;
17 |
--------------------------------------------------------------------------------
/src/stores/loading_selectors.js:
--------------------------------------------------------------------------------
1 | const LoadingSelectors = {
2 | getIsLoading: state => state.loading,
3 | };
4 |
5 | export default LoadingSelectors;
6 |
--------------------------------------------------------------------------------
/src/stores/modals_action_creators.js:
--------------------------------------------------------------------------------
1 | import ModalsConstants from '../constants/modals_constants';
2 |
3 | const ModalsActionCreators = {
4 | openSettingsModal: () => ({
5 | type: ModalsConstants.MODALS_OPEN_SETTINGS,
6 | }),
7 |
8 | closeSettingsModal: () => ({
9 | type: ModalsConstants.MODALS_CLOSE_SETTINGS,
10 | }),
11 |
12 | openSuccessModal: (successMessage) => ({
13 | type: ModalsConstants.MODALS_OPEN_SUCCESS,
14 | successMessage,
15 | }),
16 |
17 | closeSuccessModal: () => ({
18 | type: ModalsConstants.MODALS_CLOSE_SUCCESS,
19 | }),
20 |
21 | openErrorModal: (errorMessage) => ({
22 | type: ModalsConstants.MODALS_OPEN_ERROR,
23 | errorMessage,
24 | }),
25 |
26 | closeErrorModal: () => ({
27 | type: ModalsConstants.MODALS_CLOSE_ERROR,
28 | }),
29 |
30 | openCreateTaskModal: () => ({
31 | type: ModalsConstants.MODALS_OPEN_CREATE_TASK,
32 | }),
33 |
34 | closeCreateTaskModal: () => ({
35 | type: ModalsConstants.MODALS_CLOSE_CREATE_TASK,
36 | }),
37 |
38 | openManageUsersModal: () => ({
39 | type: ModalsConstants.MODALS_OPEN_MANAGE_USERS,
40 | }),
41 |
42 | closeManageUsersModal: () => ({
43 | type: ModalsConstants.MODALS_CLOSE_MANAGE_USERS,
44 | }),
45 | };
46 |
47 | export default ModalsActionCreators;
48 |
--------------------------------------------------------------------------------
/src/stores/modals_reducer.js:
--------------------------------------------------------------------------------
1 | import ModalsConstants from '../constants/modals_constants';
2 |
3 | const initialState = {
4 | showSettingsModal: false,
5 | showSuccessModal: false,
6 | successMessage: null,
7 | showErrorModal: false,
8 | errorMessage: null,
9 | showCreateTaskModal: false,
10 | showManageUsersModal: false,
11 | };
12 |
13 | const modals = (state = initialState, action) => {
14 | switch(action.type) {
15 | case ModalsConstants.MODALS_OPEN_SETTINGS:
16 | return {
17 | ...state,
18 | showSettingsModal: true,
19 | };
20 | case ModalsConstants.MODALS_CLOSE_SETTINGS:
21 | return {
22 | ...state,
23 | showSettingsModal: false,
24 | };
25 | case ModalsConstants.MODALS_OPEN_SUCCESS:
26 | return {
27 | ...state,
28 | showSuccessModal: true,
29 | successMessage: action.successMessage,
30 | };
31 | case ModalsConstants.MODALS_CLOSE_SUCCESS:
32 | return {
33 | ...state,
34 | showSuccessModal: false,
35 | successMessage: null,
36 | };
37 | case ModalsConstants.MODALS_OPEN_ERROR:
38 | return {
39 | ...state,
40 | showErrorModal: true,
41 | errorMessage: action.errorMessage,
42 | };
43 | case ModalsConstants.MODALS_CLOSE_ERROR:
44 | return {
45 | ...state,
46 | showErrorModal: false,
47 | errorMessage: null,
48 | };
49 | case ModalsConstants.MODALS_OPEN_CREATE_TASK:
50 | return {
51 | ...state,
52 | showCreateTaskModal: true,
53 | };
54 | case ModalsConstants.MODALS_CLOSE_CREATE_TASK:
55 | return {
56 | ...state,
57 | showCreateTaskModal: false,
58 | };
59 | case ModalsConstants.MODALS_OPEN_MANAGE_USERS:
60 | return {
61 | ...state,
62 | showManageUsersModal: true,
63 | };
64 | case ModalsConstants.MODALS_CLOSE_MANAGE_USERS:
65 | return {
66 | ...state,
67 | showManageUsersModal: false,
68 | };
69 | default:
70 | return state;
71 | }
72 | }
73 |
74 | export default modals;
75 |
--------------------------------------------------------------------------------
/src/stores/modals_selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const modalsSelector = state => state.modals;
4 |
5 | const ModalsSelectors = {
6 | getShowSettingsModal: createSelector(modalsSelector, (state) => state.showSettingsModal),
7 | getShowSuccessModal: createSelector(modalsSelector, (state) => state.showSuccessModal),
8 | getSuccessMessage: createSelector(modalsSelector, (state) => state.successMessage),
9 | getShowErrorModal: createSelector(modalsSelector, (state) => state.showErrorModal),
10 | getErrorMessage: createSelector(modalsSelector, (state) => state.errorMessage),
11 | getShowCreateTaskModal: createSelector(modalsSelector, (state) => state.showCreateTaskModal),
12 | getShowManageUsersModal: createSelector(modalsSelector, (state) => state.showManageUsersModal),
13 | };
14 |
15 | export default ModalsSelectors;
16 |
--------------------------------------------------------------------------------
/src/stores/schemas.js:
--------------------------------------------------------------------------------
1 | import { Schema, arrayOf } from 'normalizr';
2 |
3 | const task = new Schema('tasks', { idAttribute: 'idtask' });
4 | const arrayOfTasks = arrayOf(task);
5 |
6 | const item = new Schema('items', { idAttribute: item => item.properties._key });
7 | const arrayOfItems = arrayOf(item);
8 |
9 | const schemas = {
10 | task,
11 | arrayOfTasks,
12 | item,
13 | arrayOfItems,
14 | };
15 |
16 | export default schemas;
17 |
--------------------------------------------------------------------------------
/src/stores/settings_action_creators.js:
--------------------------------------------------------------------------------
1 | import SettingsConstants from '../constants/settings_constants';
2 |
3 | const SettingsActionCreators = {
4 | toggleSidebar: () => ({
5 | type: SettingsConstants.SETTINGS_TOGGLE_SIDEBAR
6 | }),
7 |
8 | setEditorPreference: (editor) => ({
9 | type: SettingsConstants.SETTINGS_SET_EDITOR_PREFERENCE,
10 | editor,
11 | }),
12 | };
13 |
14 | export default SettingsActionCreators;
15 |
--------------------------------------------------------------------------------
/src/stores/settings_reducer.js:
--------------------------------------------------------------------------------
1 | import SettingsConstants from '../constants/settings_constants';
2 |
3 | const initialState = {
4 | sidebar: true,
5 | editor: 'id',
6 | };
7 |
8 | const settings = (state = initialState, action) => {
9 | switch(action.type) {
10 | case SettingsConstants.SETTINGS_TOGGLE_SIDEBAR:
11 | return {
12 | ...state,
13 | sidebar: !state.sidebar,
14 | };
15 | case SettingsConstants.SETTINGS_SET_EDITOR_PREFERENCE:
16 | return {
17 | ...state,
18 | editor: action.editor,
19 | };
20 | default:
21 | return state;
22 | }
23 | };
24 |
25 | export default settings;
26 |
--------------------------------------------------------------------------------
/src/stores/settings_selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const settingsSelector = state => state.settings;
4 |
5 | const SettingsSelectors = {
6 | getSidebarSetting: createSelector(settingsSelector, (state) => state.sidebar),
7 | getEditorSetting: createSelector(settingsSelector, (state) => state.editor),
8 | };
9 |
10 | export default SettingsSelectors;
11 |
--------------------------------------------------------------------------------
/src/stores/stats_action_creators.js:
--------------------------------------------------------------------------------
1 | import { server } from '../services';
2 | import { asyncAction } from './async_action';
3 | import StatsConstants from '../constants/stats_constants';
4 |
5 | const StatsActionCreators = {
6 | fetchAllStats: asyncAction({
7 | type: StatsConstants.STATS_FETCH_ALL,
8 | asyncCall: server.fetchAllStats,
9 | showLoader: true,
10 | }),
11 | };
12 |
13 | export default StatsActionCreators;
14 |
--------------------------------------------------------------------------------
/src/stores/stats_reducer.js:
--------------------------------------------------------------------------------
1 | import StatsConstants from '../constants/stats_constants';
2 | import TasksConstants from '../constants/tasks_constants';
3 | import { AsyncStatus } from './async_action';
4 |
5 | const initialState = {
6 | byDate: [],
7 | fromDate: '',
8 | toDate: '',
9 | updatedOn: null,
10 | };
11 |
12 | const stats = (state = initialState, action) => {
13 | // Reset state when a new task is selected
14 | if (action.type === TasksConstants.TASKS_SELECT) {
15 | return initialState;
16 | }
17 |
18 | switch(action.type) {
19 | case StatsConstants.STATS_FETCH_ALL:
20 | if (action.status === AsyncStatus.SUCCESS) {
21 | const { stats, updated } = action.response;
22 | const { from, to } = action.params;
23 | return {
24 | byDate: stats,
25 | fromDate: from,
26 | toDate: to,
27 | updatedOn: updated,
28 | };
29 | }
30 | return state;
31 | default:
32 | return state;
33 | }
34 | };
35 |
36 | export default stats;
37 |
--------------------------------------------------------------------------------
/src/stores/stats_selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import pick from 'lodash.pick';
3 | import omit from 'lodash.omit';
4 |
5 | const statsSelector = state => state.stats;
6 |
7 | const countUserTotal = (statsByUser) => {
8 | return statsByUser.map(stats => {
9 | const { edit, fixed, skip, noterror } = stats;
10 | stats.total = edit + fixed + skip + noterror;
11 | return stats;
12 | });
13 | };
14 |
15 | const sortByUserTotal = (statsByUser) => {
16 | return countUserTotal(statsByUser)
17 | .sort((a, b) => b.total - a.total);
18 | };
19 |
20 | const getUserStats = (statsByDate) => {
21 | const actions = ['edit', 'skip', 'fixed', 'noterror'];
22 | const perDay = statsByDate.map(stat => omit(stat, [ 'start', ...actions ]));
23 | const statsByUser = [];
24 |
25 | perDay.forEach(stat => {
26 | Object.keys(stat).forEach(user => {
27 | const currStats = pick(stat[user], ...actions);
28 | const prevStats = statsByUser.find(s => s.user == user);
29 | if (prevStats) {
30 | actions.forEach(action => prevStats[action] += currStats[action]);
31 | } else {
32 | statsByUser.push({ user, ...currStats });
33 | }
34 | });
35 | });
36 |
37 | return statsByUser;
38 | };
39 |
40 | const StatsSelectors = {
41 | getByUser: createSelector(statsSelector, (state) => sortByUserTotal(getUserStats(state.byDate))),
42 | getByDate: createSelector(statsSelector, (state) => state.byDate),
43 | getFromDate: createSelector(statsSelector, (state) => state.fromDate),
44 | getToDate: createSelector(statsSelector, (state) => state.toDate),
45 | getUpdatedOn: createSelector(statsSelector, (state) => state.updatedOn),
46 | };
47 |
48 | export default StatsSelectors;
49 |
--------------------------------------------------------------------------------
/src/stores/store.js:
--------------------------------------------------------------------------------
1 | import { combineReducers, createStore, applyMiddleware } from 'redux';
2 | import thunk from 'redux-thunk';
3 | import createLogger from 'redux-logger';
4 |
5 | import safeStorage from '../utils/safe_storage';
6 |
7 | import user from './user_reducer';
8 | import settings from './settings_reducer';
9 | import tasks from './tasks_reducer';
10 | import items from './items_reducer';
11 | import activity from './activity_reducer';
12 | import stats from './stats_reducer';
13 | import modals from './modals_reducer';
14 | import loading from './loading_reducer';
15 | import admin from './admin_reducer';
16 |
17 | // Root reducer
18 | const rootReducer = combineReducers({
19 | user,
20 | settings,
21 | tasks,
22 | items,
23 | activity,
24 | stats,
25 | modals,
26 | loading,
27 | admin,
28 | });
29 |
30 | // Middlewares
31 | const middlewares = [thunk];
32 |
33 | if (process.env.NODE_ENV !== 'production') {
34 | const logger = createLogger();
35 | middlewares.push(logger);
36 | }
37 |
38 | // Persisted state
39 | const persistedState = {
40 | user: {
41 | isAuthenticated: false,
42 | token: safeStorage.get('token') || null,
43 | },
44 | settings: {
45 | sidebar: safeStorage.get('sidebar') || true,
46 | editor: safeStorage.get('editor') || 'id',
47 | },
48 | };
49 |
50 | // Store
51 | const store = createStore(
52 | rootReducer,
53 | persistedState,
54 | applyMiddleware(...middlewares)
55 | );
56 |
57 | // Persist change to local storage
58 | store.subscribe(() => {
59 | const state = store.getState();
60 | const { sidebar, editor } = state.settings;
61 | const { token } = state.user;
62 |
63 | if (sidebar !== safeStorage.get('sidebar')) {
64 | safeStorage.set('sidebar', sidebar);
65 | }
66 |
67 | if (editor !== safeStorage.get('editor')) {
68 | safeStorage.set('editor', editor);
69 | }
70 |
71 | if (token !== safeStorage.get('token')) {
72 | safeStorage.set('token', token);
73 | }
74 | });
75 |
76 | export default store;
77 |
--------------------------------------------------------------------------------
/src/stores/tasks_action_creators.js:
--------------------------------------------------------------------------------
1 | import { server } from '../services';
2 | import schemas from './schemas';
3 | import { asyncAction } from './async_action';
4 | import TasksConstants from '../constants/tasks_constants';
5 |
6 | const TasksActionCreators = {
7 | fetchAllTasks: asyncAction({
8 | type: TasksConstants.TASKS_FETCH_ALL,
9 | asyncCall: server.fetchAllTasks,
10 | responseSchema: { tasks: schemas.arrayOfTasks },
11 | showLoader: true,
12 | }),
13 |
14 | fetchTaskById: asyncAction({
15 | type: TasksConstants.TASKS_FETCH_BY_ID,
16 | asyncCall: server.fetchTaskById,
17 | responseSchema: schemas.task,
18 | }),
19 |
20 | selectTask: ({ idtask }) => ({
21 | type: TasksConstants.TASKS_SELECT,
22 | idtask,
23 | }),
24 |
25 | createTask: asyncAction({
26 | type: TasksConstants.TASKS_CREATE,
27 | asyncCall: server.createTask,
28 | responseSchema: schemas.task,
29 | showLoader: true,
30 | showError: false,
31 | }),
32 |
33 | updateTask: asyncAction({
34 | type: TasksConstants.TASKS_UPDATE,
35 | asyncCall: server.updateTask,
36 | responseSchema: schemas.task,
37 | showLoader: true,
38 | }),
39 |
40 | destroyTask: asyncAction({
41 | type: TasksConstants.TASKS_DESTROY,
42 | asyncCall: server.destroyTask,
43 | showLoader: true,
44 | }),
45 | };
46 |
47 | export default TasksActionCreators;
48 |
--------------------------------------------------------------------------------
/src/stores/tasks_reducer.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import union from 'lodash.union';
3 |
4 | import TasksConstants from '../constants/tasks_constants';
5 | import { AsyncStatus } from './async_action';
6 |
7 | const byId = (state = {}, action) => {
8 | switch(action.type) {
9 | case TasksConstants.TASKS_FETCH_ALL:
10 | case TasksConstants.TASKS_CREATE:
11 | case TasksConstants.TASKS_UPDATE:
12 | if (action.status === AsyncStatus.SUCCESS) {
13 | return {
14 | ...state,
15 | ...action.response.entities.tasks,
16 | };
17 | }
18 | return state;
19 | default:
20 | return state;
21 | }
22 | };
23 |
24 | const allIds = (state = [], action) => {
25 | switch(action.type) {
26 | case TasksConstants.TASKS_FETCH_ALL:
27 | if (action.status === AsyncStatus.SUCCESS) {
28 | return union(state, action.response.result.tasks);
29 | }
30 | return state;
31 | case TasksConstants.TASKS_CREATE:
32 | case TasksConstants.TASKS_UPDATE:
33 | if (action.status === AsyncStatus.SUCCESS) {
34 | return union(state, [action.response.result]);
35 | }
36 | return state;
37 | default:
38 | return state;
39 | }
40 | };
41 |
42 | const currentId = (state = null, action) => {
43 | switch(action.type) {
44 | case TasksConstants.TASKS_SELECT:
45 | return action.idtask;
46 | default:
47 | return state;
48 | }
49 | };
50 |
51 | export default combineReducers({
52 | byId,
53 | allIds,
54 | currentId,
55 | });
56 |
--------------------------------------------------------------------------------
/src/stores/tasks_selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 | import d3 from 'd3';
3 |
4 | const tasksSelector = state => state.tasks;
5 |
6 | const getAllTasks = state => (
7 | // Sort by most recent
8 | state.allIds.map(id => state.byId[id])
9 | .sort((a, b) => b.value.updated - a.value.updated)
10 | );
11 |
12 | const getLatestTaskId = state => {
13 | if (state.allIds.length !== 0) {
14 | return getAllTasks(state)[0].idtask;
15 | }
16 | }
17 |
18 | const getCompletedTasks = state => (
19 | getAllTasks(state)
20 | .filter(task => task.isCompleted)
21 | );
22 |
23 | const getActiveTasks = state => (
24 | getAllTasks(state)
25 | .filter(task => !task.isCompleted)
26 | );
27 |
28 | const getCurrentTask = state => (
29 | state.byId[state.currentId]
30 | );
31 |
32 | const getCurrentTaskId = state => (
33 | state.currentId
34 | );
35 |
36 | const getCurrentTaskSummary = state => (
37 | getCurrentTask(state).value.stats
38 | );
39 |
40 | const getCurrentTaskExtent = state => {
41 | const taskSummary = getCurrentTaskSummary(state);
42 | const createdAt = taskSummary.date * 1000;
43 |
44 | const dateFormat = d3.time.format('%Y-%m-%d');
45 | const fromDate = dateFormat(new Date(createdAt));
46 | const toDate = dateFormat(new Date());
47 |
48 | return { fromDate, toDate };
49 | };
50 |
51 | const getCurrentTaskType = state => (
52 | getCurrentTaskSummary(state).type
53 | );
54 |
55 | const TasksSelectors = {
56 | getAllTasks: createSelector(tasksSelector, getAllTasks),
57 | getLatestTaskId: createSelector(tasksSelector, getLatestTaskId),
58 | getCompletedTasks: createSelector(tasksSelector, getCompletedTasks),
59 | getActiveTasks: createSelector(tasksSelector, getActiveTasks),
60 | getCurrentTask: createSelector(tasksSelector, getCurrentTask),
61 | getCurrentTaskId: createSelector(tasksSelector, getCurrentTaskId),
62 | getCurrentTaskType: createSelector(tasksSelector, getCurrentTaskType),
63 | getCurrentTaskSummary: createSelector(tasksSelector, getCurrentTaskSummary),
64 | getCurrentTaskExtent: createSelector(tasksSelector, getCurrentTaskExtent),
65 | };
66 |
67 | export default TasksSelectors;
68 |
--------------------------------------------------------------------------------
/src/stores/user_action_creators.js:
--------------------------------------------------------------------------------
1 | import { server } from '../services';
2 | import { asyncAction } from './async_action';
3 | import UserConstants from '../constants/user_constants';
4 |
5 | const UserActionCreators = {
6 | login: (creds) => (dispatch) => {
7 | dispatch({
8 | type: UserConstants.USER_LOGGED_IN,
9 | creds,
10 | });
11 | },
12 |
13 | fetchUserDetails: asyncAction({
14 | type: UserConstants.USER_FETCH_USER_DETAILS,
15 | asyncCall: server.fetchUserDetails,
16 | }),
17 |
18 | logout: asyncAction({
19 | type: UserConstants.USER_LOGOUT,
20 | asyncCall: server.logout,
21 | }),
22 | };
23 |
24 | export default UserActionCreators;
25 |
--------------------------------------------------------------------------------
/src/stores/user_reducer.js:
--------------------------------------------------------------------------------
1 | import UserConstants from '../constants/user_constants';
2 | import { AsyncStatus } from './async_action';
3 |
4 | const initialState = {
5 | isAuthenticated: false,
6 | osmid: null,
7 | username: null,
8 | avatar: null,
9 | role: null,
10 | token: null,
11 | };
12 |
13 | const user = (state = initialState, action) => {
14 | switch(action.type) {
15 | case UserConstants.USER_LOGGED_IN:
16 | return {
17 | isAuthenticated: true,
18 | osmid: action.creds.id,
19 | username: action.creds.user,
20 | avatar: action.creds.img,
21 | role: action.creds.role,
22 | token: action.creds.token
23 | };
24 |
25 | case UserConstants.USER_FETCH_USER_DETAILS:
26 | switch(action.status) {
27 | case AsyncStatus.SUCCESS:
28 | return {
29 | isAuthenticated: true,
30 | osmid: action.response.id,
31 | username: action.response.user,
32 | avatar: action.response.img,
33 | role: action.response.role,
34 | token: action.params.token
35 | };
36 | case AsyncStatus.FAILURE:
37 | return {
38 | isAuthenticated: false,
39 | token: null,
40 | };
41 | default:
42 | return state;
43 | }
44 |
45 | case UserConstants.USER_LOGOUT:
46 | return {
47 | isAuthenticated: false,
48 | token: null,
49 | };
50 |
51 | default:
52 | return state;
53 | }
54 | };
55 |
56 | export default user;
57 |
--------------------------------------------------------------------------------
/src/stores/user_selectors.js:
--------------------------------------------------------------------------------
1 | import { createSelector } from 'reselect';
2 |
3 | const userSelector = state => state.user;
4 |
5 | const UserSelectors = {
6 | getIsAuthenticated: createSelector(userSelector, (state) => state.isAuthenticated),
7 | getOsmId: createSelector(userSelector, (state) => state.osmid),
8 | getUsername: createSelector(userSelector, (state) => state.username),
9 | getAvatar: createSelector(userSelector, (state) => state.avatar),
10 | getRole: createSelector(userSelector, (state) => state.role),
11 | getToken: createSelector(userSelector, (state) => state.token),
12 | };
13 |
14 | export default UserSelectors;
15 |
--------------------------------------------------------------------------------
/src/utils/d3_graph.js:
--------------------------------------------------------------------------------
1 | import d3 from 'd3';
2 |
3 | export default {
4 | create: function(el) {
5 | el = d3.select(el);
6 | var svg = el.append('svg')
7 | .attr('width', this._getContainerWidth(el))
8 | .attr('height', this._getContainerHeight());
9 |
10 | svg.append('g')
11 | .attr('class', 'd3-area')
12 | .attr('transform', 'translate(' + this._margin.left + ',' + this._margin.top + ')');
13 |
14 | // Displays date when brushing
15 | el.append('strong')
16 | .style('position', 'absolute')
17 | .attr('class', 'tooltip fill-darken3 hidden round pad1x pad0y small strong z10');
18 | },
19 |
20 | resize: function(el) {
21 | el = d3.select(el);
22 | el.select('svg')
23 | .attr('width', this._getContainerWidth(el));
24 |
25 | this.x.range([0, this._getWidth(el)]);
26 | },
27 |
28 | noData: function(el) {
29 | el.append('text')
30 | .text('No data found.')
31 | .attr('transform', 'translate(-40,0)');
32 | },
33 |
34 | update: function(el, state, params, fetchData) {
35 | params = (!this._empty(params)) ? params : false;
36 | el = d3.select(el);
37 |
38 | var g = el.selectAll('.d3-area');
39 |
40 | // TODO This is dirty
41 | g.html('');
42 | if (!state.data.length) return this.noData(g);
43 | var _this = this;
44 | var tooltip = el.select('.tooltip');
45 |
46 | // Normalize the data that came in
47 | var data = state.data.map(function(d) {
48 | d.date = new Date(d.start * 1000);
49 | return d;
50 | });
51 |
52 | this.x = d3.time.scale().range([0, this._getWidth(el)]);
53 | this.y = d3.scale.linear().range([this._getHeight(), 0]);
54 |
55 | var xAxis = d3.svg.axis().scale(this.x).orient('bottom');
56 | var yAxis = d3.svg.axis()
57 | .ticks(5)
58 | .scale(this.y)
59 | .orient('left');
60 |
61 | var brush = d3.svg.brush().x(this.x);
62 |
63 | brush.on('brushend', function() {
64 | tooltip.classed('hidden', true);
65 | var extent = brush.extent();
66 | var from = this._dateFormat(extent[0]);
67 | var to = this._dateFormat(extent[1]);
68 | var query = [
69 | this._queryDateFormat(extent[0]),
70 | this._queryDateFormat(extent[1])
71 | ];
72 | // fetchData(query[0], query[1]);
73 | }.bind(this));
74 |
75 | brush.on('brush', function() {
76 | tooltip.classed('hidden', false);
77 | var extent = brush.extent();
78 | var from = this._dateFormat(extent[0]);
79 | var to = this._dateFormat(extent[1]);
80 |
81 | tooltip
82 | .style('left', (this.x(extent[1]) + 75) + 'px')
83 | .style('top', '20px')
84 | .text(from + ' - ' + to);
85 | }.bind(this));
86 |
87 | var area = d3.svg.area()
88 | .interpolate('monotone')
89 | .x(function(d) { return this.x(d.date); }.bind(this))
90 | .y0(this._getHeight())
91 | .y1(function(d) { return this.y(d.value); }.bind(this));
92 |
93 | this.x.domain(d3.extent(data.map(function(d) { return d.date; })));
94 | this.y.domain([0, d3.max(data.map(function(d) { return d.value; }))]);
95 |
96 | // Build out the SVG elements for our graph.
97 | var path = g.selectAll('path')
98 | .data([data]);
99 |
100 | path.enter().append('path')
101 | .attr('class', 'area')
102 | .attr('d', area);
103 |
104 | var graphComponents = g.selectAll('g')
105 | .data([data]);
106 |
107 | graphComponents.enter().append('g')
108 | .attr('class', 'y axis')
109 | .call(yAxis);
110 |
111 | graphComponents.enter().append('g')
112 | .attr('class', 'x axis')
113 | .attr('transform', 'translate(0,' + this._getHeight() + ')')
114 | .call(xAxis);
115 |
116 | var from, to, query = [
117 | this._queryDateFormat(data[0].date),
118 | this._queryDateFormat(data[data.length - 1].date)
119 | ];
120 |
121 | // Set extents to brush based on params +
122 | // Trigger graphUpdated with params passed by
123 | // the query URL. Don't set the graph if params
124 | // do not exist or the param extents match the data extents.
125 | if (params &&
126 | params.from !== query[0] &&
127 | params.to !== query[1]) {
128 | var parse = d3.time.format.utc('%Y-%m-%d').parse;
129 | brush.extent([parse(params.from), parse(params.to)]);
130 | from = this._dateFormat(parse(params.from));
131 | to = this._dateFormat(parse(params.to));
132 | query = [params.from, params.to];
133 | } else {
134 | from = this._dateFormat(data[0].date);
135 | to = this._dateFormat(data[data.length - 1].date);
136 | }
137 |
138 | var gBrush = graphComponents.enter().append('g')
139 | .attr('class', 'x brush')
140 | .call(brush);
141 |
142 | gBrush.selectAll('rect')
143 | .attr('y', -6)
144 | .attr('height', this._getHeight() + 7);
145 |
146 | // Remove old elements as needed.
147 | path.exit().remove();
148 | graphComponents.exit().remove();
149 |
150 | // If params are set, trigger the brush to draw
151 | // initial extents on the graph.
152 | if (params) gBrush.call(brush.event);
153 | // fetchData(query[0], query[1]);
154 | },
155 |
156 | destroy: function(el) {},
157 |
158 | // Dimensions
159 | _margin: { top: 10, right: 0, bottom: 20, left: 40 },
160 |
161 | _getHeight: function() {
162 | return 100 - (this._margin.top - this._margin.bottom);
163 | },
164 |
165 | _getWidth: function(el) {
166 | return parseInt(el.style('width'), 10) - this._margin.left - this._margin.right;
167 | },
168 |
169 | _getContainerWidth: function(el) {
170 | return this._getWidth(el) + this._margin.left + this._margin.right;
171 | },
172 |
173 | _getContainerHeight: function() {
174 | return this._getHeight() + this._margin.top + this._margin.bottom;
175 | },
176 |
177 | _dateFormat: function(date) {
178 | return d3.time.format.utc('%b %e, %Y')(date);
179 | },
180 |
181 | _queryDateFormat: function(date) {
182 | return d3.time.format.utc('%Y-%m-%d')(date);
183 | },
184 |
185 | _empty: function(obj) {
186 | for (var prop in obj) {
187 | if (obj.hasOwnProperty(prop)) return false;
188 | }
189 | return true;
190 | }
191 | };
192 |
--------------------------------------------------------------------------------
/src/utils/safe_storage.js:
--------------------------------------------------------------------------------
1 | const get = (key) => {
2 | try {
3 | const serializedValue = localStorage.getItem(key);
4 | if (serializedValue === null) {
5 | return undefined;
6 | }
7 | return JSON.parse(serializedValue);
8 | } catch (err) {
9 | console.error('Could not read from localStorage.');
10 | return undefined;
11 | }
12 | };
13 |
14 | const set = (key, value) => {
15 | try {
16 | const serializedValue = JSON.stringify(value);
17 | localStorage.setItem(key, serializedValue);
18 | } catch (err) {
19 | console.error('Could not write to localStorage.');
20 | }
21 | };
22 |
23 | const remove = (key) => {
24 | try {
25 | localStorage.removeItem(key);
26 | } catch (err) {
27 | console.error('Could not delete from localStorage.');
28 | }
29 | };
30 |
31 | const safeStorage = {
32 | get,
33 | set,
34 | remove,
35 | };
36 |
37 | export default safeStorage;
38 |
--------------------------------------------------------------------------------