├── src ├── App.scss ├── components │ ├── AdminPane │ │ ├── Manage │ │ │ ├── GridBlocks │ │ │ │ ├── .gitignore │ │ │ │ ├── ChallengeListBlock │ │ │ │ │ ├── ChallengeListBlock.scss │ │ │ │ │ └── Messages.js │ │ │ │ ├── CommentsBlock │ │ │ │ │ ├── CommentsBlock.scss │ │ │ │ │ └── Messages.js │ │ │ │ ├── LeaderboardBlock │ │ │ │ │ ├── LeaderboardBlock.scss │ │ │ │ │ └── Messages.js │ │ │ │ ├── ProjectAboutBlock │ │ │ │ │ ├── ProjectAboutBlock.scss │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── ProjectAboutBlock.js │ │ │ │ ├── BurndownChartBlock │ │ │ │ │ ├── BurndownChartBlock.scss │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── BurndownChartBlock.js │ │ │ │ ├── MenuControlDivider.js │ │ │ │ ├── StatusRadarBlock │ │ │ │ │ ├── StatusRadarBlock.scss │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── StatusRadarBlock.js │ │ │ │ ├── CalendarHeatmapBlock │ │ │ │ │ ├── CalendarHeatmapBlock.scss │ │ │ │ │ └── Messages.js │ │ │ │ ├── ChallengeTasksBlock │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── ChallengeTasksBlock.js │ │ │ │ ├── MenuControl.js │ │ │ │ ├── ProjectCountBlock │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── ProjectCountBlock.js │ │ │ │ ├── ProjectOverviewBlock │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── ProjectOverviewBlock.scss │ │ │ │ ├── RecentActivityBlock │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── RecentActivityBlock.js │ │ │ │ ├── ProjectListBlock │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── ProjectListBlock.scss │ │ │ │ ├── CompletionProgressBlock │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── CompletionProgressBlock.js │ │ │ │ └── ChallengeOverviewBlock │ │ │ │ │ ├── ChallengeOverviewBlock.scss │ │ │ │ │ └── Messages.js │ │ │ ├── StepNavigation │ │ │ │ ├── StepNavigation.scss │ │ │ │ └── Messages.js │ │ │ ├── GridBlockPicker │ │ │ │ ├── GridBlockPicker.scss │ │ │ │ └── Messages.js │ │ │ ├── ReviewTask │ │ │ │ ├── ReviewTask.scss │ │ │ │ └── Messages.js │ │ │ ├── EditProject │ │ │ │ └── EditProject.scss │ │ │ ├── ChallengeKeywords │ │ │ │ ├── ChallengeKeywords.scss │ │ │ │ ├── ChallengeKeywords.test.js │ │ │ │ └── ChallengeKeywords.js │ │ │ ├── Messages.js │ │ │ ├── ProjectOverview │ │ │ │ ├── ProjectOverview.scss │ │ │ │ └── Messages.js │ │ │ ├── CalendarHeatmap │ │ │ │ ├── Messages.js │ │ │ │ └── CalendarHeatmap.scss │ │ │ ├── CompletionRadar │ │ │ │ └── Messages.js │ │ │ ├── KeywordAutosuggestInput │ │ │ │ ├── Messages.js │ │ │ │ └── KeywordAutosuggestInput.scss │ │ │ ├── ManageTasks │ │ │ │ └── Messages.js │ │ │ ├── BurndownChart │ │ │ │ └── Messages.js │ │ │ ├── ChallengeList │ │ │ │ ├── Messages.js │ │ │ │ └── ChallengeList.scss │ │ │ ├── ProjectDashboard │ │ │ │ └── Messages.js │ │ │ ├── TaskUploadingProgress │ │ │ │ └── Messages.js │ │ │ ├── ManageChallenges │ │ │ │ └── EditChallenge │ │ │ │ │ └── EditChallenge.scss │ │ │ ├── ProjectLeaderboard │ │ │ │ └── ProjectLeaderboard.js │ │ │ ├── ChallengeOwnerLeaderboard │ │ │ │ └── ChallengeOwnerLeaderboard.js │ │ │ ├── Dashboard │ │ │ │ ├── Messages.js │ │ │ │ └── Dashboard.scss │ │ │ ├── ProjectsDashboard │ │ │ │ ├── ProjectsDashboard.scss │ │ │ │ └── Messages.js │ │ │ ├── DashboardFilterToggle │ │ │ │ └── DashboardFilterToggle.js │ │ │ ├── ChallengeAnalysisTable │ │ │ │ └── ChallengeAnalysisTable.scss │ │ │ ├── ChallengeTaskMap │ │ │ │ └── Messages.js │ │ │ ├── TaskAnalysisTable │ │ │ │ └── TaskAnalysisTable.scss │ │ │ ├── ChallengeDashboard │ │ │ │ └── Messages.js │ │ │ ├── ViewTask │ │ │ │ └── ViewTask.js │ │ │ ├── ProjectManagers │ │ │ │ ├── ProjectManagers.scss │ │ │ │ └── Messages.js │ │ │ ├── RebuildTasksControl │ │ │ │ └── RebuildTasksControl.scss │ │ │ ├── ProjectCard │ │ │ │ └── Messages.js │ │ │ ├── ProjectList │ │ │ │ └── ProjectList.scss │ │ │ └── ChallengeFilterGroup │ │ │ │ └── ChallengeFilterGroup.js │ │ ├── MetricsOverview │ │ │ ├── Messages.js │ │ │ └── MetricsOverview.scss │ │ └── HOCs │ │ │ ├── WithWideScreenOption │ │ │ └── WithWideScreenOption.js │ │ │ └── WithComboSearch │ │ │ └── WithComboSearch.js │ ├── Sprites │ │ └── Sprites.scss │ ├── TaskPane │ │ ├── TaskMap │ │ │ ├── TaskMap.scss │ │ │ └── TaskMap.test.js │ │ ├── TaskPane.scss │ │ ├── ActiveTaskDetails │ │ │ ├── CollapsibleSection │ │ │ │ └── CollapsibleSection.scss │ │ │ ├── ActiveTaskControls │ │ │ │ ├── TaskCancelEditingControl │ │ │ │ │ ├── TaskCancelEditingControl.scss │ │ │ │ │ └── Messages.js │ │ │ │ ├── MoreOptionsControl │ │ │ │ │ └── Messages.js │ │ │ │ ├── TaskCommentInput │ │ │ │ │ ├── Messages.js │ │ │ │ │ ├── TaskCommentInput.scss │ │ │ │ │ ├── TaskCommentInput.test.js │ │ │ │ │ └── TaskCommentInput.js │ │ │ │ ├── TaskEditControl │ │ │ │ │ └── Messages.js │ │ │ │ ├── TaskNextControl │ │ │ │ │ ├── Messages.js │ │ │ │ │ └── TaskNextControl.scss │ │ │ │ ├── TaskSkipControl │ │ │ │ │ └── Messages.js │ │ │ │ ├── TaskFixedControl │ │ │ │ │ └── Messages.js │ │ │ │ ├── TaskCompletionStep2 │ │ │ │ │ ├── TaskCompletionStep2.scss │ │ │ │ │ └── Messages.js │ │ │ │ ├── TaskAlreadyFixedControl │ │ │ │ │ └── Messages.js │ │ │ │ ├── TaskTooHardControl │ │ │ │ │ └── Messages.js │ │ │ │ ├── TaskFalsePositiveControl │ │ │ │ │ └── Messages.js │ │ │ │ └── TaskCompletionStep1 │ │ │ │ │ └── TaskCompletionStep1.scss │ │ │ ├── ReviewTaskControls │ │ │ │ ├── ReviewTaskControls.scss │ │ │ │ └── Messages.js │ │ │ ├── KeyboardShortcutReference │ │ │ │ ├── Messages.js │ │ │ │ └── KeyboardShortcutReference.scss │ │ │ ├── TaskStatusIndicator │ │ │ │ └── Messages.js │ │ │ └── Messages.js │ │ ├── MobileTaskDetails │ │ │ └── Messages.js │ │ ├── TaskRandomnessControl │ │ │ ├── Messages.js │ │ │ └── TaskRandomnessControl.scss │ │ ├── VirtualChallengeNameLink │ │ │ └── Messages.js │ │ ├── TaskLatLon │ │ │ └── TaskLatLon.scss │ │ ├── TaskTrackControls │ │ │ └── Messages.js │ │ ├── TaskManageControls │ │ │ ├── TaskManageControls.scss │ │ │ └── Messages.js │ │ ├── OwnerContactLink │ │ │ └── Messages.js │ │ ├── ChallengeShareControls │ │ │ ├── ChallengeShareControls.scss │ │ │ └── ChallengeShareControls.test.js │ │ ├── TaskLocationMap │ │ │ ├── TaskLocationMap.scss │ │ │ └── TaskLocationMap.js │ │ └── ChallengeNameLink │ │ │ └── ChallengeNameLink.js │ ├── LoadMoreButton │ │ ├── LoadMoreButton.scss │ │ └── Messages.js │ ├── ChallengePane │ │ ├── ChallengeResultList │ │ │ ├── LoadMoreButton.scss │ │ │ ├── SortChallengesSelector.scss │ │ │ └── Messages.js │ │ ├── ChallengeFilterSubnav │ │ │ ├── OtherKeywordsOption.js │ │ │ ├── Messages.js │ │ │ └── ChallengeFilterSubnav.test.js │ │ └── ChallengePane.test.js │ ├── Bulma │ │ ├── SimpleDropdown.scss │ │ ├── LabeledProgressBar.scss │ │ ├── Steps.scss │ │ ├── Menu.scss │ │ ├── RJSFFormFieldAdapter │ │ │ └── Messages.js │ │ ├── TriStateCheckbox.js │ │ ├── Menu.js │ │ ├── SvgControl.js │ │ └── Modal.js │ ├── HOCs │ │ ├── WithEditor │ │ │ ├── __snapshots__ │ │ │ │ └── WithEditor.test.js.snap │ │ │ └── WithEditor.js │ │ ├── WithErrors │ │ │ ├── __snapshots__ │ │ │ │ └── WithErrors.test.js.snap │ │ │ └── WithErrors.js │ │ ├── WithProjects │ │ │ ├── __snapshots__ │ │ │ │ └── WithProjects.test.js.snap │ │ │ ├── WithProjects.js │ │ │ └── WithProjects.test.js │ │ ├── WithLayerSources │ │ │ └── Messages.js │ │ ├── WithCurrentUser │ │ │ └── __snapshots__ │ │ │ │ └── WithCurrentUser.test.js.snap │ │ ├── WithStatus │ │ │ ├── WithStatus.js │ │ │ └── __snapshots__ │ │ │ │ └── WithStatus.test.js.snap │ │ ├── WithCurrentTask │ │ │ └── __snapshots__ │ │ │ │ └── WithCurrentTask.test.js.snap │ │ ├── WithTaskCenterPoint │ │ │ └── WithTaskCenterPoint.js │ │ ├── WithClusteredTasks │ │ │ └── WithClusteredTasks.js │ │ ├── WithProgress │ │ │ └── WithProgress.js │ │ └── WithCommandInterpreter │ │ │ └── WithCommandInterpreter.test.js │ ├── InsetMap │ │ ├── InsetMap.scss │ │ └── InsetMap.test.js │ ├── BusySpinner │ │ ├── BusySpinner.scss │ │ └── BusySpinner.js │ ├── SignInButton │ │ ├── SignInButton.scss │ │ └── Messages.js │ ├── EnhancedMap │ │ ├── FitBoundsControl │ │ │ ├── FitBoundsControl.scss │ │ │ └── Messages.js │ │ ├── LayerToggle │ │ │ ├── Messages.js │ │ │ └── LayerToggle.scss │ │ └── MapPane │ │ │ └── MapPane.js │ ├── HelpPopout │ │ ├── Messages.js │ │ └── HelpPopout.scss │ ├── ShareLink │ │ ├── Messages.js │ │ └── ShareLink.scss │ ├── MapillaryViewer │ │ └── MapillaryViewer.scss │ ├── CommentList │ │ ├── Messages.js │ │ ├── CommentCountBadge │ │ │ ├── CommentCountBadge.test.js │ │ │ ├── CommentCountBadge.scss │ │ │ └── CommentCountBadge.js │ │ └── CommentList.scss │ ├── AutosuggestTextBox │ │ ├── Messages.js │ │ └── AutosuggestTextBox.scss │ ├── UserProfile │ │ ├── TopChallenges │ │ │ └── Messages.js │ │ ├── SavedTasks │ │ │ └── Messages.js │ │ ├── SavedChallenges │ │ │ └── Messages.js │ │ ├── Messages.js │ │ ├── PersonalInfo.js │ │ └── UserProfile.test.js │ ├── ChallengeSearchMap │ │ └── Messages.js │ ├── Ribbon │ │ ├── Ribbon.js │ │ └── Ribbon.scss │ ├── PastDurationSelector │ │ ├── Messages.js │ │ └── PastDurationSelector.scss │ ├── ChallengeProgress │ │ ├── ChallengeProgress.scss │ │ ├── Messages.js │ │ └── ChallengeProgress.test.js │ ├── Navbar │ │ ├── AccountNavItem │ │ │ ├── Messages.js │ │ │ ├── AccountNavItem.scss │ │ │ └── AccountNavItem.test.js │ │ ├── Messages.js │ │ └── Navbar.test.js │ ├── PageNotFound │ │ ├── Messages.js │ │ └── PageNotFound.scss │ ├── ScreenTooNarrow │ │ ├── Messages.js │ │ ├── ScreenTooNarrow.scss │ │ └── ScreenTooNarrow.js │ ├── CongratulateModal │ │ └── Messages.js │ ├── ConfirmAction │ │ ├── ConfirmAction.scss │ │ └── Messages.js │ ├── QuickTextBox │ │ └── QuickTextBox.scss │ ├── SvgSymbol │ │ ├── SvgSymbol.test.js │ │ └── SvgSymbol.js │ ├── ProjectLeaderboard │ │ └── ProjectLeaderboard.js │ ├── MobileFilterMenu │ │ └── MobileFilterMenu.scss │ ├── ChallengeLeaderboard │ │ └── ChallengeLeaderboard.js │ ├── MobileNotSupported │ │ ├── Messages.js │ │ └── MobileNotSupported.scss │ ├── ErrorModal │ │ ├── ErrorModal.test.js │ │ └── ErrorModal.scss │ ├── ActivityTimeline │ │ └── ActivityTimeline.scss │ ├── MarkdownContent │ │ └── MarkdownContent.js │ ├── HomePane │ │ └── Messages.js │ └── Leaderboard │ │ └── Messages.js ├── services │ ├── Server │ │ ├── RequestStatus.js │ │ ├── RequestCache.js │ │ └── Route.test.js │ ├── Challenge │ │ ├── ChallengeActions.js │ │ ├── ChallengeType │ │ │ ├── Messages.js │ │ │ └── ChallengeType.js │ │ ├── ChallengeDifficulty │ │ │ ├── Messages.js │ │ │ └── ChallengeDifficulty.js │ │ ├── ChallengeZoom │ │ │ └── ChallengeZoom.js │ │ ├── ChallengeLocation │ │ │ └── Messages.js │ │ ├── ChallengeStatus │ │ │ └── Messages.js │ │ ├── ChallengeBasemap │ │ │ └── Messages.js │ │ └── ChallengeKeywords │ │ │ └── Messages.js │ ├── Task │ │ ├── TaskLoadMethod │ │ │ ├── Messages.js │ │ │ └── TaskLoadMethod.js │ │ ├── TaskPriority │ │ │ ├── Messages.js │ │ │ └── TaskPriority.js │ │ ├── TaskAction │ │ │ └── TaskAction.js │ │ └── TaskStatus │ │ │ └── Messages.js │ ├── Dashboard │ │ ├── ChallengeFilter │ │ │ ├── Messages.js │ │ │ └── ChallengeFilter.js │ │ ├── ProjectFilter │ │ │ ├── Messages.js │ │ │ └── ProjectFilter.js │ │ └── Dashboard.js │ ├── Project │ │ └── GroupType │ │ │ └── Messages.js │ ├── Search │ │ └── Messages.js │ ├── User │ │ └── Locale │ │ │ └── Messages.js │ ├── Editor │ │ └── Messages.js │ ├── VisibleLayer │ │ └── VisibleLayer.js │ ├── Activity │ │ ├── ActivityItemTypes │ │ │ ├── Messages.js │ │ │ └── ActivityItemTypes.js │ │ └── ActivityActionTypes │ │ │ └── Messages.js │ ├── OSMUser │ │ └── OSMUser.js │ └── Leaderboard │ │ └── Leaderboard.js ├── setupTests.js ├── index.scss └── interactions │ ├── Overpass │ ├── Messages.js │ └── AsValidatableOverpass.js │ ├── Project │ └── AsManageableProject.js │ ├── Challenge │ └── AsMappableChallenge.js │ └── User │ ├── AsMappingUser.js │ └── AsEndUser.js ├── public ├── favicon.ico └── manifest.json ├── chimp.example.js ├── .travis.yml ├── features ├── pages │ ├── Page.js │ └── OpenStreetMap.js ├── steps │ └── HomePageSteps.js └── Signin.feature ├── .gitignore └── LICENSE.md /src/App.scss: -------------------------------------------------------------------------------- 1 | .App { 2 | width: 100%; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/.gitignore: -------------------------------------------------------------------------------- 1 | contrib/ 2 | -------------------------------------------------------------------------------- /src/components/Sprites/Sprites.scss: -------------------------------------------------------------------------------- 1 | .sprites { 2 | display: none; 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Zverik/maproulette3/master/public/favicon.ico -------------------------------------------------------------------------------- /src/components/TaskPane/TaskMap/TaskMap.scss: -------------------------------------------------------------------------------- 1 | .task-map { 2 | position: relative; 3 | } 4 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ChallengeListBlock/ChallengeListBlock.scss: -------------------------------------------------------------------------------- 1 | .challenge-list-block { 2 | } 3 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/CommentsBlock/CommentsBlock.scss: -------------------------------------------------------------------------------- 1 | .comments-block { 2 | .export-control { 3 | margin-right: 10px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskPane.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .task-pane { 4 | position: relative; 5 | background-color: $grey-lighter; 6 | } 7 | -------------------------------------------------------------------------------- /chimp.example.js: -------------------------------------------------------------------------------- 1 | // Copy this file to chimp.js and update the settings. 2 | 3 | module.exports = { 4 | mr3URL: "https://maproulette.mydevserver.com/mr3", 5 | } 6 | -------------------------------------------------------------------------------- /src/components/LoadMoreButton/LoadMoreButton.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins.scss'; 2 | 3 | button.load-more-button { 4 | @include invert-on-hover($green, $white); 5 | } 6 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/LeaderboardBlock/LeaderboardBlock.scss: -------------------------------------------------------------------------------- 1 | .leaderboard-block { 2 | .past-duration-selector { 3 | margin-right: 10px; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/components/ChallengePane/ChallengeResultList/LoadMoreButton.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins.scss'; 2 | 3 | button.load-more-button { 4 | @include invert-on-hover($green, $white); 5 | } 6 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/StepNavigation/StepNavigation.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .step-navigation { 4 | display: flex; 5 | justify-content: space-between; 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Bulma/SimpleDropdown.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .SimpleDropdown { 4 | .dropdown-trigger { 5 | &:hover { 6 | cursor: pointer; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/services/Server/RequestStatus.js: -------------------------------------------------------------------------------- 1 | const RequestStatus = Object.freeze({ 2 | inProgress: 'in progress', 3 | success: 'success', 4 | error: 'error', 5 | }) 6 | 7 | export default RequestStatus 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | sudo: false 3 | node_js: 4 | - "8" 5 | cache: 6 | yarn: true 7 | directories: 8 | - node_modules 9 | script: 10 | - yarn run build 11 | - yarn test 12 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ProjectAboutBlock/ProjectAboutBlock.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins.scss'; 2 | 3 | .project-about-block { 4 | .grid-block__content { 5 | @include markdown-content(); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlockPicker/GridBlockPicker.scss: -------------------------------------------------------------------------------- 1 | .grid-block-picker { 2 | margin-right: 15px; 3 | 4 | .dropdown-trigger { 5 | .basic-dropdown-indicator { 6 | top: -1px; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ReviewTask/ReviewTask.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../variables.scss'; 2 | 3 | .admin__manage.review-task { 4 | .task-pane { 5 | border: 1px solid $grey-lightest; 6 | overflow: hidden; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/EditProject/EditProject.scss: -------------------------------------------------------------------------------- 1 | .edit-project { 2 | .title { 3 | display: flex; 4 | justify-content: flex-start; 5 | 6 | .busy-spinner { 7 | margin-left: 1em; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/HOCs/WithEditor/__snapshots__/WithEditor.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mapStateToProps provides an editor prop with the current open editor 1`] = ` 4 | Object { 5 | "editor": 1, 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeActions.js: -------------------------------------------------------------------------------- 1 | // This needs to be separated from Challenge to avoid a circular 2 | // dependency with Project. 3 | export const RECEIVE_CHALLENGES = 'RECEIVE_CHALLENGES' 4 | export const REMOVE_CHALLENGE = 'REMOVE_CHALLENGE' 5 | -------------------------------------------------------------------------------- /src/components/InsetMap/InsetMap.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | .inset-map { 4 | .leaflet-container { 5 | height: 200px; 6 | border-radius: $radius-medium; 7 | 8 | .leaflet-pane { 9 | z-index: $intralayer-bump; 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/BusySpinner/BusySpinner.scss: -------------------------------------------------------------------------------- 1 | @import '../../theme.scss'; 2 | 3 | .busy-spinner { 4 | &.inline { 5 | display: inline-block; 6 | margin-left: 5px; 7 | margin-right: 5px; 8 | } 9 | 10 | .busy-spinner-icon { 11 | @include loader; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/BurndownChartBlock/BurndownChartBlock.scss: -------------------------------------------------------------------------------- 1 | .burndown-chart-block { 2 | .burndown-chart { 3 | height: 375px; 4 | 5 | div { 6 | max-height: 100%; 7 | } 8 | 9 | svg { 10 | max-height: 370px; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/SignInButton/SignInButton.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | @import '../../mixins.scss'; 3 | 4 | .button.signin-button { 5 | @include invert-on-hover($primary, $white); 6 | 7 | &.white-on-green { 8 | @include invert-on-hover($white, $green); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/EnhancedMap/FitBoundsControl/FitBoundsControl.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .fit-bounds-control { 4 | svg { 5 | width: 19px; 6 | height: auto; 7 | fill: $green; 8 | } 9 | 10 | &:hover { 11 | background-color: $grey-lightest-more; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/CollapsibleSection/CollapsibleSection.scss: -------------------------------------------------------------------------------- 1 | .collapsible-section { 2 | &__heading { 3 | position: relative; 4 | display: flex; 5 | justify-content: space-between; 6 | 7 | &:hover { 8 | cursor: pointer; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/components/HelpPopout/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with HelpPopout. 5 | */ 6 | export default defineMessages({ 7 | help: { 8 | id: "HelpPopout.control.label", 9 | defaultMessage: "Help" 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/ShareLink/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ShareLink. 5 | */ 6 | export default defineMessages({ 7 | copy: { 8 | id: 'ShareLink.controls.copy.label', 9 | defaultMessage: "Copy", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeKeywords/ChallengeKeywords.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .challenge-keywords { 4 | display: flex; 5 | flex-wrap: wrap; 6 | 7 | .tag { 8 | margin-right: 5px; 9 | color: $grey; 10 | background-color: $grey-lightest-less; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/components/SignInButton/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with SigninButton. 5 | */ 6 | export default defineMessages({ 7 | control: { 8 | id: 'SignIn.control.label', 9 | defaultMessage: "Sign in", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/MapillaryViewer/MapillaryViewer.scss: -------------------------------------------------------------------------------- 1 | @import "variables.scss"; 2 | 3 | .mapillary-viewer { 4 | .message-header { 5 | .button.icon-only .control-icon svg { 6 | fill: $white; 7 | } 8 | } 9 | 10 | &__license { 11 | font-size: $size-8; 12 | text-align: right; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with Admin Manage. 5 | */ 6 | export default defineMessages({ 7 | manageHeader: { 8 | id: 'Admin.manage.header', 9 | defaultMessage: "Create & Manage", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/Bulma/LabeledProgressBar.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | .labeled-progress { 4 | .description { 5 | display: flex; 6 | justify-content: space-between; 7 | 8 | .progress-made { 9 | .value { 10 | font-weight: $weight-semibold; 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectOverview/ProjectOverview.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .project-list { 4 | .project-overview { 5 | &__description { 6 | font-size: $size-6; 7 | margin-bottom: 20px; 8 | } 9 | 10 | &__controls { 11 | margin-top: 20px; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/components/CommentList/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with CommentList 5 | */ 6 | export default defineMessages({ 7 | viewTaskLabel: { 8 | id: "CommentList.controls.viewTask.label", 9 | defaultMessage: "View Task" 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/MenuControlDivider.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | /** 4 | * Renders a divider between MenuControls in a grid block 5 | */ 6 | export default class MenuControlDivider extends Component { 7 | render() { 8 | return
9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/StatusRadarBlock/StatusRadarBlock.scss: -------------------------------------------------------------------------------- 1 | .status-radar-block { 2 | .completion-radar-chart { 3 | margin: 0; 4 | min-width: 200px; 5 | height: 350px; 6 | 7 | div { 8 | max-height: 100%; 9 | } 10 | 11 | svg { 12 | max-height: 345px; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ReviewTask/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ReviewTask 5 | */ 6 | export default defineMessages({ 7 | reviewTask: { 8 | id: "Admin.ReviewTask.header", 9 | defaultMessage: "Review Tasks", 10 | }, 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /src/components/AutosuggestTextBox/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with AutosuggestTextBox 5 | */ 6 | export default defineMessages({ 7 | noResults: { 8 | id: "AutosuggestTextBox.labels.noResults", 9 | defaultMessage: "No matches" 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/LoadMoreButton/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with LoadMoreButton. 5 | */ 6 | export default defineMessages({ 7 | moreResultsLabel: { 8 | id: "General.controls.moreResults.label", 9 | defaultMessage: "More Results", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlockPicker/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with GridBlockPicker 5 | */ 6 | export default defineMessages({ 7 | pickerLabel: { 8 | id: "GridBlockPicker.menuLabel", 9 | defaultMessage: "Add Widget", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/UserProfile/TopChallenges/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TopChallenges 5 | */ 6 | export default defineMessages({ 7 | header: { 8 | id: "UserProfile.topChallenges.header", 9 | defaultMessage: "Your Top Challenges", 10 | }, 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /src/components/EnhancedMap/FitBoundsControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with FitBoundsControl 5 | */ 6 | export default defineMessages({ 7 | tooltip: { 8 | id: "FitBoundsControl.tooltip", 9 | defaultMessage: "Fit map to task features" 10 | }, 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /src/components/TaskPane/MobileTaskDetails/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with MobileTaskDetails. 5 | */ 6 | export default defineMessages({ 7 | instructions: { 8 | id: 'MobileTask.subheading.instructions', 9 | defaultMessage: 'Instructions', 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/CalendarHeatmap/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with CalendarHeatmap. 5 | */ 6 | export default defineMessages({ 7 | heading: { 8 | id: "CalendarHeatmap.heading", 9 | defaultMessage: "Daily Heatmap: Task Completion", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/CompletionRadar/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with CompletionRadar. 5 | */ 6 | export default defineMessages({ 7 | heading: { 8 | id: "CompletionRadar.heading", 9 | defaultMessage: "Tasks Completed: {taskCount, number}", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/AdminPane/MetricsOverview/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with MetricsOverview 5 | */ 6 | export default defineMessages({ 7 | evaluatedLabel: { 8 | id: "Metrics.tasks.evaluatedByUser.label", 9 | defaultMessage: "Tasks Evaluated by Users", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/HOCs/WithErrors/__snapshots__/WithErrors.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mapStateToProps maps currentErrors to errors 1`] = ` 4 | Object { 5 | "errors": Array [ 6 | Object { 7 | "defaultMessage": "A foo bar baz error.", 8 | "id": "Errors.foo.bar.baz", 9 | }, 10 | ], 11 | } 12 | `; 13 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskRandomnessControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskRandomnessControl. 5 | */ 6 | export default defineMessages({ 7 | taskLoadByLabel: { 8 | id: 'Challenge.controls.taskLoadBy.label', 9 | defaultMessage: "Load tasks by:", 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/Bulma/Steps.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .steps:not(.is-hollow) { 4 | .steps-segment { 5 | .steps-marker.clickable:not(.is-hollow) { 6 | &:hover { 7 | cursor: pointer; 8 | box-shadow: 0px 2px 6px 0px $box-shadow-color; 9 | transition: box-shadow .25s ease-in-out; 10 | } 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/ChallengeSearchMap/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeSearchMap 5 | */ 6 | export default defineMessages({ 7 | startChallengeLabel: { 8 | id: "ChallengeSearchMap.controls.startChallenge.label", 9 | defaultMessage: "Start Challenge" 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import Enzyme, { shallow, render, mount } from 'enzyme' 2 | import Adapter from 'enzyme-adapter-react-16' 3 | 4 | // React 16 Enzyme adapter 5 | Enzyme.configure({ adapter: new Adapter() }) 6 | 7 | // Make Enzyme functions available in all test files without importing 8 | global.shallow = shallow 9 | global.render = render 10 | global.mount = mount 11 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskCancelEditingControl/TaskCancelEditingControl.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../variables.scss'; 2 | 3 | .active-task-details .cancel-control { 4 | margin-top: 15px; 5 | margin-bottom: 15px; 6 | 7 | svg { 8 | height: 18px; 9 | width: auto; 10 | vertical-align: middle; 11 | margin-right: 10px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/TaskPane/VirtualChallengeNameLink/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with VirtualChallengeNameLink 5 | */ 6 | export default defineMessages({ 7 | virtualChallengeTooltip: { 8 | id: 'ActiveTask.indicators.virtualChallenge.tooltip', 9 | defaultMessage: 'Virtual Challenge', 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/KeywordAutosuggestInput/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with KeywordAutosuggestInput 5 | */ 6 | export default defineMessages({ 7 | addKeywordPlaceholder: { 8 | id: "KeywordAutosuggestInput.controls.addKeyword.placeholder", 9 | defaultMessage: "Add keyword" 10 | }, 11 | }) 12 | -------------------------------------------------------------------------------- /src/components/HOCs/WithProjects/__snapshots__/WithProjects.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mapStateToProps maps projects 1`] = ` 4 | Object { 5 | "projects": Array [ 6 | Object { 7 | "id": "123", 8 | "name": "first", 9 | }, 10 | Object { 11 | "id": "456", 12 | "name": "second", 13 | }, 14 | ], 15 | } 16 | `; 17 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/MoreOptionsControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with MoreOptionsControls 5 | */ 6 | export default defineMessages({ 7 | moreOptionsLabel: { 8 | id: "Task.controls.moreOptions.label", 9 | defaultMessage: "More Options" 10 | }, 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskCommentInput/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskCommentInput. 5 | */ 6 | export default defineMessages({ 7 | placeholder: { 8 | id: 'Task.controls.completionComment.placeholder', 9 | defaultMessage: "Optional comment", 10 | }, 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /src/components/Ribbon/Ribbon.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import classNames from 'classnames' 3 | import './Ribbon.css' 4 | 5 | export default class Ribbon extends Component { 6 | render() { 7 | return ( 8 |
9 | {this.props.children} 10 |
11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/index.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | } 7 | 8 | .loading { 9 | display: flex; 10 | align-items: center; 11 | height: 100vh; 12 | background-color: $green; 13 | color: $white; 14 | 15 | svg { 16 | fill: $white; 17 | } 18 | 19 | h1.title { 20 | color: $white; 21 | } 22 | 23 | &__content { 24 | flex: auto; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/CalendarHeatmapBlock/CalendarHeatmapBlock.scss: -------------------------------------------------------------------------------- 1 | .calendar-heatmap-block { 2 | &.months-3 { 3 | .calendar-heatmap { 4 | max-width: 250px; 5 | } 6 | } 7 | 8 | &.months-6 { 9 | .calendar-heatmap { 10 | max-width: 450px; 11 | } 12 | } 13 | 14 | &.months-9 { 15 | .calendar-heatmap { 16 | max-width: 650px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/PastDurationSelector/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with PastDurationSelector 5 | */ 6 | export default defineMessages({ 7 | pastMonthsOption: { 8 | id: "PastDurationSelector.pastMonths.selectOption", 9 | defaultMessage: "Past {months, plural, one {Month} =12 {Year} other {# Months}}", 10 | }, 11 | }) 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeType/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeType. 5 | */ 6 | export default defineMessages({ 7 | challenge: { 8 | id: "Challenge.type.challenge", 9 | defaultMessage: "Challenge", 10 | }, 11 | survey: { 12 | id: "Challenge.type.survey", 13 | defaultMessage: "Survey", 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskLatLon/TaskLatLon.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .task-lat-lon { 4 | 5 | &__label { 6 | margin-right: 0.5em; 7 | } 8 | 9 | button.button.task-lat-lon__copy-button { 10 | height: 1em; 11 | margin-left: 0.5em; 12 | 13 | svg { 14 | fill: $green; 15 | } 16 | 17 | &:hover { 18 | svg { 19 | fill: $black; 20 | } 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/services/Task/TaskLoadMethod/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskLoadMethod 5 | */ 6 | export default defineMessages({ 7 | random: { 8 | id: 'Task.loadByMethod.random', 9 | defaultMessage: "Random", 10 | }, 11 | 12 | proximity: { 13 | id: 'Task.loadByMethod.proximity', 14 | defaultMessage: "Proximity", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/ChallengeProgress/ChallengeProgress.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | .challenge-task-progress { 4 | margin-bottom: 20px; 5 | font-size: $size-9; 6 | height: 55px; 7 | position: relative; 8 | 9 | &__completed-indicator { 10 | position: absolute; 11 | top: 3px; 12 | right: 2px; 13 | fill: $green; 14 | stroke: $white; 15 | height: 20px; 16 | width: auto; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/components/ChallengeProgress/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeProgress 5 | */ 6 | export default defineMessages({ 7 | available: { 8 | id: "Task.fauxStatus.available", 9 | defaultMessage: "Available" 10 | }, 11 | 12 | tooltipLabel: { 13 | id: "ChallengeProgress.tooltip.label", 14 | defaultMessage: "Tasks" 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/Navbar/AccountNavItem/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with AccountNavItem. 5 | */ 6 | export default defineMessages({ 7 | profile: { 8 | id: 'Navbar.links.userProfile', 9 | defaultMessage: "User Profile", 10 | }, 11 | 12 | signout: { 13 | id: 'Navbar.links.signout', 14 | defaultMessage: "Sign out", 15 | }, 16 | }) 17 | 18 | -------------------------------------------------------------------------------- /src/components/UserProfile/SavedTasks/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with SavedTasks. 5 | */ 6 | export default defineMessages({ 7 | header: { 8 | id: "UserProfile.savedTasks.header", 9 | defaultMessage: "Tracked Tasks", 10 | }, 11 | unsave: { 12 | id: "Task.unsave.control.tooltip", 13 | defaultMessage: "Stop Tracking", 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/InsetMap/InsetMap.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import InsetMap from './InsetMap' 3 | 4 | const basicProps = { 5 | centerPoint: { 6 | lat: 0, 7 | lng: 0, 8 | }, 9 | fixedZoom: 15, 10 | } 11 | 12 | test('renders an inset map at the given centerpoint and zoom', () => { 13 | const wrapper = shallow( 14 | 15 | ) 16 | 17 | expect(wrapper.find('Map').exists()).toBe(true) 18 | }) 19 | -------------------------------------------------------------------------------- /src/components/UserProfile/SavedChallenges/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with SavedChallenges. 5 | */ 6 | export default defineMessages({ 7 | header: { 8 | id: "UserProfile.savedChallenges.header", 9 | defaultMessage: "Saved Challenges", 10 | }, 11 | unsave: { 12 | id: "Challenge.controls.unsave.tooltip", 13 | defaultMessage: "Unsave Challenge", 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /src/services/Dashboard/ChallengeFilter/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeFilter 5 | */ 6 | export default defineMessages({ 7 | visible: { 8 | id: "Dashboard.ChallengeFilter.visible.label", 9 | defaultMessage: "Visible", 10 | }, 11 | 12 | pinned: { 13 | id: "Dashboard.ChallengeFilter.pinned.label", 14 | defaultMessage: "Pinned", 15 | }, 16 | }) 17 | 18 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ManageTasks/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ManageTasks 5 | */ 6 | export default defineMessages({ 7 | editTaskTooltip: { 8 | id: "Admin.Task.controls.editTask.tooltip", 9 | defaultMessage: "Edit Task", 10 | }, 11 | 12 | editTaskLabel: { 13 | id: "Admin.Task.controls.editTask.label", 14 | defaultMessage: "Edit", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskTrackControls/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskTrackControls 5 | */ 6 | export default defineMessages({ 7 | trackLabel: { 8 | id: "Task.controls.track.label", 9 | defaultMessage: "Track this Task", 10 | }, 11 | untrackLabel: { 12 | id: "Task.controls.untrack.label", 13 | defaultMessage: "Stop tracking this Task", 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/BurndownChart/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with BurndownChart. 5 | */ 6 | export default defineMessages({ 7 | heading: { 8 | id: "BurndownChart.heading", 9 | defaultMessage: "Tasks Remaining: {taskCount, number}", 10 | }, 11 | 12 | tooltip: { 13 | id: "BurndownChart.tooltip", 14 | defaultMessage: "Tasks Remaining", 15 | }, 16 | }) 17 | 18 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ChallengeTasksBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeTasksBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.ChallengeTasksBlock.label", 9 | defaultMessage: "Tasks", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.ChallengeTasksBlock.title", 14 | defaultMessage: "Tasks", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/LeaderboardBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with LeaderboardBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.LeaderboardBlock.label", 9 | defaultMessage: "Leaderboard", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.LeaderboardBlock.title", 14 | defaultMessage: "Leaderboard", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/HOCs/WithLayerSources/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with WithLayerSources 5 | */ 6 | export default defineMessages({ 7 | challengeDefault: { 8 | id: "LayerSource.challengeDefault.label", 9 | defaultMessage: "Challenge Default", 10 | }, 11 | userDefault: { 12 | id: "LayerSource.userDefault.label", 13 | defaultMessage: "Your Default", 14 | }, 15 | }) 16 | 17 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskEditControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskEditControl. 5 | */ 6 | export default defineMessages({ 7 | editLabel: { 8 | id: 'Task.controls.edit.label', 9 | defaultMessage: 'Edit', 10 | }, 11 | 12 | editTooltip: { 13 | id: 'Task.controls.edit.tooltip', 14 | defaultMessage: 'Edit', 15 | }, 16 | }) 17 | 18 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskNextControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskNextControl. 5 | */ 6 | export default defineMessages({ 7 | nextLabel: { 8 | id: 'Task.controls.next.label', 9 | defaultMessage: 'Next Task', 10 | }, 11 | nextTooltip: { 12 | id: 'Task.controls.next.tooltip', 13 | defaultMessage: 'Next Task', 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskSkipControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskSkipControl. 5 | */ 6 | export default defineMessages({ 7 | skipLabel: { 8 | id: 'Task.controls.skip.label', 9 | defaultMessage: 'Skip', 10 | }, 11 | skipTooltip: { 12 | id: 'Task.controls.skip.tooltip', 13 | defaultMessage: 'Skip Task', 14 | }, 15 | }) 16 | 17 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskManageControls/TaskManageControls.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .active-task-controls__task-manage-controls { 4 | text-align: center; 5 | 6 | h3 { 7 | color: $coral; 8 | margin-top: 15px; 9 | font-weight: $weight-semibold; 10 | } 11 | 12 | &__options { 13 | a:not(:last-child) { 14 | border-right: 2px solid $grey-lightest; 15 | padding-right: 8px; 16 | margin-right: 8px; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/MenuControl.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | /** 4 | * Wraps a grid block control with the appropriate CSS classes to ensure it 5 | * displays properly in the block's dropdown menu 6 | */ 7 | export default class MenuControl extends Component { 8 | render() { 9 | return( 10 |
11 | {this.props.children} 12 |
13 | ) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ProjectCountBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectCountBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.ProjectCountBlock.label", 9 | defaultMessage: "Project Count", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.ProjectCountBlock.title", 14 | defaultMessage: "Project Count", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ProjectOverviewBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectOverviewBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.ProjectOverviewBlock.label", 9 | defaultMessage: "Overview", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.ProjectOverviewBlock.title", 14 | defaultMessage: "Overview", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/services/Task/TaskPriority/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskPriority. 5 | */ 6 | export default defineMessages({ 7 | high: { 8 | id: "Task.priority.high", 9 | defaultMessage: "High" 10 | }, 11 | medium: { 12 | id: "Task.priority.medium", 13 | defaultMessage: "Medium" 14 | }, 15 | low: { 16 | id: "Task.priority.low", 17 | defaultMessage: "Low" 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskNextControl/TaskNextControl.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../variables.scss'; 2 | 3 | .active-task-details .next-control { 4 | margin-top: 5px; 5 | margin-bottom: 10px; 6 | 7 | svg { 8 | height: 18px; 9 | width: auto; 10 | vertical-align: middle; 11 | margin-left: 10px; 12 | } 13 | 14 | &.icon-only { 15 | margin-top: 0; 16 | 17 | svg { 18 | margin-left: 0; 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeList/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeList 5 | */ 6 | export default defineMessages({ 7 | addChallengeLabel: { 8 | id: 'Admin.Project.controls.addChallenge.label', 9 | defaultMessage: "Add Challenge", 10 | }, 11 | 12 | noChallenges: { 13 | id: "Admin.ChallengeList.noChallenges", 14 | defaultMessage: "No Challenges", 15 | }, 16 | }) 17 | 18 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/RecentActivityBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with RecentActivityBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.RecentActivityBlock.label", 9 | defaultMessage: "Recent Activity", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.RecentActivityBlock.title", 14 | defaultMessage: "Recent Activity", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/StatusRadarBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with StatusRadarBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.StatusRadarBlock.label", 9 | defaultMessage: "Status Radar", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.StatusRadarBlock.title", 14 | defaultMessage: "Completion Status Distribution", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskFixedControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskFixedControl. 5 | */ 6 | export default defineMessages({ 7 | fixedLabel: { 8 | id: 'Task.controls.fixed.label', 9 | defaultMessage: "I fixed it!", 10 | }, 11 | 12 | fixedTooltip: { 13 | id: 'Task.controls.fixed.tooltip', 14 | defaultMessage: 'I fixed it!', 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/TaskPane/OwnerContactLink/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with OwnerContactLink 5 | */ 6 | export default defineMessages({ 7 | contactOwnerLabel: { 8 | id: 'Task.controls.contactOwner.label', 9 | defaultMessage: 'Contact Owner', 10 | }, 11 | 12 | contactLinkLabel: { 13 | id: 'Task.controls.contactLink.label', 14 | defaultMessage: 'Message {owner} through OSM', 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/ChallengePane/ChallengeFilterSubnav/OtherKeywordsOption.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import SearchBox from '../../SearchBox/SearchBox' 3 | 4 | export default class OtherKeywordsOption extends Component { 5 | render() { 6 | return ( 7 |
8 | 9 | 10 |
11 | ) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ReviewTaskControls/ReviewTaskControls.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../variables.scss'; 2 | 3 | .review-task-controls { 4 | &__control-block { 5 | display: flex; 6 | justify-content: space-between; 7 | 8 | .button { 9 | margin-bottom: 10px; 10 | } 11 | } 12 | 13 | .review-task-controls__task-comment { 14 | margin-bottom: 20px; 15 | } 16 | 17 | button.post-comment-control { 18 | margin: -5px 0px 15px 10px; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/services/Project/GroupType/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with GroupType 5 | */ 6 | export default defineMessages({ 7 | admin: { 8 | id: "Project.GroupType.admin", 9 | defaultMessage: "Admin", 10 | }, 11 | write: { 12 | id: "Project.GroupType.write", 13 | defaultMessage: "Write", 14 | }, 15 | read: { 16 | id: "Project.GroupType.read", 17 | defaultMessage: "Read", 18 | }, 19 | }) 20 | -------------------------------------------------------------------------------- /features/pages/Page.js: -------------------------------------------------------------------------------- 1 | export class Page { 2 | constructor() { 3 | this.title = "MR3 Page" 4 | } 5 | 6 | baseURL() { 7 | return process.env["chimp.mr3URL"] 8 | } 9 | 10 | get app() { 11 | return browser.element(".App") 12 | } 13 | 14 | appIsVisible() { 15 | this.app.isExisting() 16 | } 17 | 18 | waitForApp() { 19 | this.app.waitForVisible(10000) 20 | } 21 | 22 | open(path) { 23 | browser.url(this.baseURL() + path) 24 | this.waitForApp() 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/BurndownChartBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with BurndownChartBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.BurndownChartBlock.label", 9 | defaultMessage: "Burndown Chart", 10 | }, 11 | title: { 12 | id: "GridBlocks.BurndownChartBlock.title", 13 | defaultMessage: "Tasks Remaining: {taskCount, number}", 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/CalendarHeatmapBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with CalendarHeatmapBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.CalendarHeatmapBlock.label", 9 | defaultMessage: "Daily Heatmap", 10 | }, 11 | title: { 12 | id: "GridBlocks.CalendarHeatmapBlock.title", 13 | defaultMessage: "Daily Heatmap: Task Completion", 14 | }, 15 | }) 16 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectDashboard/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectDashboard 5 | */ 6 | export default defineMessages({ 7 | editProjectLabel: { 8 | id: "Admin.ProjectDashboard.controls.edit.label", 9 | defaultMessage: "Edit", 10 | }, 11 | 12 | addChallengeLabel: { 13 | id: "Admin.ProjectDashboard.controls.addChallenge.label", 14 | defaultMessage: "Add Challenge", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/TaskUploadingProgress/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskUploadingProgress 5 | */ 6 | export default defineMessages({ 7 | creatingTasks: { 8 | id: 'Admin.TaskUploadProgress.uploadingTasks.header', 9 | defaultMessage: "Rebuilding Tasks", 10 | }, 11 | 12 | tasksCreated: { 13 | id: 'Admin.TaskUploadProgress.tasksUploaded.label', 14 | defaultMessage: "tasks uploaded", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/KeyboardShortcutReference/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with KeyboardShortcutReference. 5 | */ 6 | export default defineMessages({ 7 | control: { 8 | id: "KeyboardShortcuts.control.label", 9 | defaultMessage: "Keyboard Shortcuts", 10 | }, 11 | 12 | keyboardShortcuts: { 13 | id: 'ActiveTask.keyboardShortcuts.label', 14 | defaultMessage: 'View Keyboard Shortcuts', 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/AutosuggestTextBox/AutosuggestTextBox.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .autosuggest-text-box { 4 | width: 100%; 5 | 6 | &__input-wrapper { 7 | position: relative; 8 | width: 100%; 9 | 10 | input { 11 | padding-right: 40px; 12 | } 13 | 14 | .busy-spinner { 15 | position: absolute; 16 | top: 8px; 17 | right: 5px; 18 | } 19 | } 20 | 21 | &__no-results { 22 | font-size: $size-7; 23 | margin: 0 1em; 24 | color: $grey-light; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskCancelEditingControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskCancelEditingControl. 5 | */ 6 | export default defineMessages({ 7 | cancelEditingLabel: { 8 | id: 'Task.controls.cancelEditing.label', 9 | defaultMessage: "Go Back", 10 | }, 11 | cancelEditingTooltip: { 12 | id: 'Task.controls.cancelEditing.tooltip', 13 | defaultMessage: "Go Back", 14 | }, 15 | }) 16 | 17 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskCompletionStep2/TaskCompletionStep2.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../variables.scss'; 2 | 3 | .task-completion-controls { 4 | display: flex; 5 | flex-direction: column; 6 | 7 | button { 8 | margin-bottom: 10px; 9 | } 10 | 11 | &__cancel { 12 | margin-top: 15px; 13 | margin-bottom: 15px; 14 | 15 | svg { 16 | height: 18px; 17 | width: auto; 18 | vertical-align: middle; 19 | margin-right: 10px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskAlreadyFixedControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskAlreadyFixedControl. 5 | */ 6 | export default defineMessages({ 7 | alreadyFixedLabel: { 8 | id: 'Task.controls.alreadyFixed.label', 9 | defaultMessage: "Already fixed", 10 | }, 11 | 12 | alreadyFixedTooltip: { 13 | id: 'Task.controls.alreadyFixed.tooltip', 14 | defaultMessage: "Already fixed", 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ManageChallenges/EditChallenge/EditChallenge.scss: -------------------------------------------------------------------------------- 1 | .edit-challenge { 2 | position: relative; 3 | min-width: 500px; 4 | 5 | ul.steps { 6 | position: absolute; 7 | right: -30px; 8 | 9 | @media(max-width: 750px) { 10 | position: static; 11 | &:not(.is-vertical).is-narrow.is-right { 12 | justify-content: center; 13 | } 14 | } 15 | } 16 | 17 | .rjsf .array-field .priority-rule { 18 | input[type=text] { 19 | max-width: 150px; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/Bulma/Menu.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | aside.menu { 4 | ul.menu-list { 5 | li { 6 | position: relative; 7 | 8 | svg.menu-list__active-indicator { 9 | fill: $green; 10 | width: 0.5em; 11 | height: auto; 12 | position: absolute; 13 | top: 13px; 14 | left: 5px; 15 | } 16 | 17 | a { 18 | padding: 0.5em 0.75em 0.5em 1.25em; 19 | } 20 | 21 | label { 22 | color: $grey-dark; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskTooHardControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskTooHardControl. 5 | */ 6 | export default defineMessages({ 7 | tooHardLabel: { 8 | id: 'Task.controls.tooHard.label', 9 | defaultMessage: "Too difficult / Couldn't see", 10 | }, 11 | 12 | tooHardTooltip: { 13 | id: 'Task.controls.tooHard.tooltip', 14 | defaultMessage: "Too difficult / Couldn't see", 15 | }, 16 | }) 17 | 18 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskFalsePositiveControl/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskFalsePositiveControl. 5 | */ 6 | export default defineMessages({ 7 | falsePositiveLabel: { 8 | id: 'Task.controls.falsePositive.label', 9 | defaultMessage: 'Not an Issue', 10 | }, 11 | 12 | falsePositiveTooltip: { 13 | id: 'Task.controls.falsePositive.tooltip', 14 | defaultMessage: 'Not an Issue', 15 | }, 16 | }) 17 | 18 | -------------------------------------------------------------------------------- /src/interactions/Overpass/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with Overpass interactions 5 | */ 6 | export default defineMessages({ 7 | noOverpassTurboShortcuts: { 8 | id: "Admin.EditChallenge.overpass.errors.noTurboShortcuts", 9 | defaultMessage: "Overpass Turbo shortcuts are not supported. If you wish to use them, please visit Overpass Turbo and test your query, then choose Export -> Query -> Standalone -> Copy and then paste that here.", 10 | }, 11 | }) 12 | 13 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectLeaderboard/ProjectLeaderboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Leaderboard from '../../../Leaderboard/Leaderboard' 3 | 4 | export default class ProjectLeaderboard extends Component { 5 | render() { 6 | return 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/components/PageNotFound/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with PageNotFound 5 | */ 6 | export default defineMessages({ 7 | missingPage: { 8 | id: "PageNotFound.message", 9 | defaultMessage: "Sorry, nothing here but open ocean.", 10 | }, 11 | 12 | returnTo: { 13 | id: "PageNotFound.returnTo", 14 | defaultMessage: "Return to", 15 | }, 16 | 17 | homePage: { 18 | id: "PageNotFound.homePage", 19 | defaultMessage: "home page", 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskRandomnessControl/TaskRandomnessControl.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .task-randomness-control { 4 | margin: 15px 0px; 5 | 6 | .control { 7 | display: flex; 8 | justify-content: center; 9 | flex-wrap: wrap; 10 | } 11 | 12 | &__prompt { 13 | margin-right: 0.5em; 14 | } 15 | 16 | &__options { 17 | display: flex; 18 | flex-direction: column; 19 | justify-content: flex-start; 20 | align-items: center; 21 | 22 | input { 23 | margin: 0 5px; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/ScreenTooNarrow/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ScreenTooNarrow 5 | */ 6 | export default defineMessages({ 7 | header: { 8 | id: "ScreenTooNarrow.header", 9 | defaultMessage: "Please widen your browser window" 10 | }, 11 | 12 | message: { 13 | id: "ScreenTooNarrow.message", 14 | defaultMessage: "This page is not yet compatible with smaller screens. Please expand your browser window or switch to a larger device or display." 15 | }, 16 | }) 17 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeOwnerLeaderboard/ChallengeOwnerLeaderboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Leaderboard from '../../../Leaderboard/Leaderboard' 3 | 4 | export default class ChallengeOwnerLeaderboard extends Component { 5 | render() { 6 | return 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/services/Dashboard/ProjectFilter/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectFilter 5 | */ 6 | export default defineMessages({ 7 | visible: { 8 | id: "Dashboard.ProjectFilter.visible.label", 9 | defaultMessage: "Visible", 10 | }, 11 | 12 | owner: { 13 | id: "Dashboard.ProjectFilter.owner.label", 14 | defaultMessage: "Owned", 15 | }, 16 | 17 | pinned: { 18 | id: "Dashboard.ProjectFilter.pinned.label", 19 | defaultMessage: "Pinned", 20 | }, 21 | }) 22 | 23 | -------------------------------------------------------------------------------- /src/services/Task/TaskAction/TaskAction.js: -------------------------------------------------------------------------------- 1 | import { TaskStatus } from '../TaskStatus/TaskStatus' 2 | import _fromPairs from 'lodash/fromPairs' 3 | import _map from 'lodash/map' 4 | import _keys from 'lodash/keys' 5 | 6 | /** 7 | * Returns an actions object with everything zeroed-out to represent that there 8 | * are no actions. 9 | */ 10 | export const zeroTaskActions = function() { 11 | const actions = 12 | _fromPairs(_map(_keys(TaskStatus), statusName => [statusName, 0])) 13 | actions.total = 0 14 | actions.available = 0 15 | 16 | return actions 17 | } 18 | -------------------------------------------------------------------------------- /src/services/Server/RequestCache.js: -------------------------------------------------------------------------------- 1 | import Cache from 'stale-lru-cache' 2 | 3 | /** 4 | * The primary purpose of the cache is simply to reduce duplicate requests to 5 | * the serverf that can naturally occur during a single user action from 6 | * decoupled components that don't know anything about data other components 7 | * might already have retrieved. So the maxAge of the cache is set very low. 8 | */ 9 | export const cache = new Cache({ 10 | maxSize: 100, 11 | maxAge: 10, // seconds 12 | }) 13 | 14 | export const resetCache = () => { 15 | cache.reset() 16 | } 17 | -------------------------------------------------------------------------------- /src/components/HOCs/WithCurrentUser/__snapshots__/WithCurrentUser.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mapStateToProps adds isLoggedIn and isSuperUser fields to the user 1`] = ` 4 | Object { 5 | "user": Object { 6 | "id": 456, 7 | "isLoggedIn": true, 8 | "isSuperUser": true, 9 | }, 10 | } 11 | `; 12 | 13 | exports[`mapStateToProps provides the current user in denormalized form 1`] = ` 14 | Object { 15 | "user": Object { 16 | "id": 456, 17 | "isLoggedIn": undefined, 18 | "isSuperUser": undefined, 19 | }, 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskManageControls/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskManagementControls. 5 | */ 6 | export default defineMessages({ 7 | heading: { 8 | id: "Task.management.heading", 9 | defaultMessage: "Management Options", 10 | }, 11 | reviewLabel: { 12 | id: "Task.management.controls.review.label", 13 | defaultMessage: "Review", 14 | }, 15 | modifyLabel: { 16 | id: "Task.management.controls.modify.label", 17 | defaultMessage: "Modify", 18 | }, 19 | }) 20 | 21 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/CommentsBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with CommentsBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.CommentsBlock.label", 9 | defaultMessage: "Comments", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.CommentsBlock.title", 14 | defaultMessage: "Comments", 15 | }, 16 | 17 | exportLabel: { 18 | id: "GridBlocks.CommentsBlock.controls.export.label", 19 | defaultMessage: "Export", 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/CongratulateModal/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with CongratulateModal. 5 | */ 6 | export default defineMessages({ 7 | header: { 8 | id: "CongratulateModal.header", 9 | defaultMessage: "Congratulations!" 10 | }, 11 | 12 | primaryMessage: { 13 | id: "CongratulateModal.primaryMessage", 14 | defaultMessage: "Challenge is complete" 15 | }, 16 | 17 | dismiss: { 18 | id: "CongratulateModal.control.dismiss.label", 19 | defaultMessage: "Continue" 20 | }, 21 | }) 22 | 23 | -------------------------------------------------------------------------------- /src/services/Search/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with Sort. 5 | */ 6 | export default defineMessages({ 7 | name: { 8 | id: "Challenge.sort.name", 9 | defaultMessage: "Name", 10 | }, 11 | created: { 12 | id: "Challenge.sort.created", 13 | defaultMessage: "Newest", 14 | }, 15 | popularity: { 16 | id: "Challenge.sort.popularity", 17 | defaultMessage: "Popular", 18 | }, 19 | default: { 20 | id: "Challenge.sort.default", 21 | defaultMessage: "Smart", 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ProjectOverviewBlock/ProjectOverviewBlock.scss: -------------------------------------------------------------------------------- 1 | .project-overview-block { 2 | .grid-block__content { 3 | .columns { 4 | margin: -0.5em 0; 5 | 6 | &:first-child { 7 | margin-top: 0; 8 | } 9 | 10 | &:last-child { 11 | margin-bottom: 0; 12 | } 13 | 14 | .column { 15 | padding: 0.25em; 16 | } 17 | } 18 | } 19 | 20 | .project-overview__status.status-section { 21 | padding-top: 5px; 22 | } 23 | 24 | &__project-description { 25 | margin-top: 20px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /src/**/*.css 6 | 7 | # Imagery 8 | src/imagery.json 9 | src/defaultLayers.json 10 | src/customLayers.json 11 | 12 | # testing 13 | /coverage 14 | chimp.js 15 | 16 | # production 17 | /build 18 | /MR3React.tgz 19 | 20 | # misc 21 | .DS_Store 22 | .env.local 23 | .env.development 24 | .env.development.local 25 | .env.test 26 | .env.test.local 27 | .env.production 28 | .env.production.local 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | 34 | *.swp 35 | -------------------------------------------------------------------------------- /src/components/ConfirmAction/ConfirmAction.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .confirm-action { 4 | display: inline-flex; 5 | justify-content: flex-start; 6 | 7 | .message .message-body { 8 | background-color: $white; 9 | } 10 | 11 | &__prompt { 12 | color: $grey-dark; 13 | font-size: $size-5; 14 | } 15 | 16 | &__controls { 17 | margin-top: 20px; 18 | display: flex; 19 | justify-content: flex-end; 20 | 21 | button.button { 22 | margin-right: 15px; 23 | 24 | &:last-child { 25 | margin-right: 0; 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ProjectListBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectListBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.ProjectListBlock.label", 9 | defaultMessage: "Project List", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.ProjectListBlock.title", 14 | defaultMessage: "Projects", 15 | }, 16 | 17 | searchPlaceholder: { 18 | id: "GridBlocks.ProjectListBlock.search.placeholder", 19 | defaultMessage: "Search", 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ChallengeListBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeListBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.ChallengeListBlock.label", 9 | defaultMessage: "Challenges", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.ChallengeListBlock.title", 14 | defaultMessage: "Challenges", 15 | }, 16 | 17 | searchPlaceholder: { 18 | id: "GridBlocks.ChallengeListBlock.search.placeholder", 19 | defaultMessage: "Search", 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/HOCs/WithStatus/WithStatus.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import _get from 'lodash/get' 3 | import { FETCHING_CHALLENGES_STATUS, 4 | CHECKING_LOGIN_STATUS } 5 | from '../../../services/Status/Status' 6 | 7 | export const mapStateToProps = state => { 8 | return { 9 | fetchingChallenges: 10 | _get(state, `currentStatus.${FETCHING_CHALLENGES_STATUS}`, []), 11 | 12 | checkingLoginStatus: 13 | _get(state, `currentStatus.${CHECKING_LOGIN_STATUS}`, false), 14 | } 15 | } 16 | 17 | export default WrappedComponent => 18 | connect(mapStateToProps)(WrappedComponent) 19 | -------------------------------------------------------------------------------- /src/components/QuickTextBox/QuickTextBox.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .quick-text-box { 4 | display: flex; 5 | justify-content: space-between; 6 | align-items: center; 7 | 8 | .control-wrapper { 9 | margin-right: 10px; 10 | flex: 1 1 100%; 11 | } 12 | 13 | button.button { 14 | height: 45px; 15 | margin-left: 5px; 16 | 17 | &.is-small { 18 | height: 32px; 19 | } 20 | } 21 | 22 | &__done-button.button { 23 | svg { 24 | fill: $green; 25 | } 26 | } 27 | 28 | &__cancel-button.button { 29 | svg { 30 | fill: $coral; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectOverview/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectOverview 5 | */ 6 | export default defineMessages({ 7 | creationDate: { 8 | id: "Admin.Project.fields.creationDate.label", 9 | defaultMessage: "Created:", 10 | }, 11 | 12 | lastModifiedDate: { 13 | id: "Admin.Project.fields.lastModifiedDate.label", 14 | defaultMessage: "Modified:", 15 | }, 16 | 17 | deleteProject: { 18 | id: "Admin.Project.controls.delete.label", 19 | defaultMessage: "Delete Project", 20 | }, 21 | }) 22 | 23 | -------------------------------------------------------------------------------- /src/components/HelpPopout/HelpPopout.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | .help-popout { 4 | &.dropdown.popout-right { 5 | .menu-wrapper { 6 | position: absolute; 7 | left: 45px; 8 | } 9 | } 10 | 11 | &--control { 12 | margin: 0 5px; 13 | 14 | svg { 15 | fill: $grey-light; 16 | width: 20px; 17 | height: auto; 18 | } 19 | 20 | &:hover { 21 | svg { 22 | fill: $grey-dark; 23 | } 24 | } 25 | } 26 | 27 | &--content { 28 | min-width: 300px; 29 | padding: 10px; 30 | color: $grey-darkest; 31 | font-size: $size-6; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/TaskStatusIndicator/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskStatusIndicator 5 | */ 6 | export default defineMessages({ 7 | statusPrompt: { 8 | id: 'ActiveTask.subheading.status', 9 | defaultMessage: "Existing Status", 10 | }, 11 | 12 | statusTooltip: { 13 | id: 'ActiveTask.controls.status.tooltip', 14 | defaultMessage: "Existing Status", 15 | }, 16 | 17 | viewChangeset: { 18 | id: 'ActiveTask.controls.viewChangset.label', 19 | defaultMessage: "View Changeset", 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeDifficulty/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeDifficulty. 5 | */ 6 | export default defineMessages({ 7 | easy: { 8 | id: "Challenge.difficulty.easy", 9 | defaultMessage: "Easy", 10 | }, 11 | normal: { 12 | id: "Challenge.difficulty.normal", 13 | defaultMessage: "Normal", 14 | }, 15 | expert: { 16 | id: "Challenge.difficulty.expert", 17 | defaultMessage: "Expert", 18 | }, 19 | any: { 20 | id: "Challenge.difficulty.any", 21 | defaultMessage: "Any", 22 | } 23 | }) 24 | 25 | -------------------------------------------------------------------------------- /src/components/HOCs/WithCurrentTask/__snapshots__/WithCurrentTask.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`mapDispatchToProps maps some functions 1`] = ` 4 | Object { 5 | "completeTask": [Function], 6 | "fetchOSMUser": [Function], 7 | "loadTask": [Function], 8 | "nextTask": [Function], 9 | "refreshTask": [Function], 10 | } 11 | `; 12 | 13 | exports[`mapStateToProps maps task from the taskId in the route 1`] = ` 14 | Object { 15 | "challengeId": undefined, 16 | "task": Object { 17 | "id": 987, 18 | "name": "task987", 19 | "parent": 123, 20 | }, 21 | "taskId": 987, 22 | } 23 | `; 24 | -------------------------------------------------------------------------------- /src/components/PastDurationSelector/PastDurationSelector.scss: -------------------------------------------------------------------------------- 1 | @import 'theme.scss'; 2 | 3 | .past-duration-selector { 4 | .dropdown-trigger { 5 | line-height: 24px; 6 | 7 | .button.is-outlined { 8 | @include invert-on-hover($grey-light, $white); 9 | font-weight: $weight-normal; 10 | 11 | .dropdown-indicator { 12 | @include arrow($grey-light); 13 | height: 10px; 14 | width: 10px; 15 | position: relative; 16 | margin-left: 0.5em; 17 | bottom: 2px; 18 | } 19 | } 20 | } 21 | 22 | .dropdown-item { 23 | font-weight: $weight-normal; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskCommentInput/TaskCommentInput.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../variables.scss'; 2 | 3 | .task-completion-comment { 4 | width: 100%; 5 | 6 | input { 7 | width: 100%; 8 | font-size: $size-6; 9 | color: $grey; 10 | padding: 8px 12px; 11 | border: 2px solid $grey; 12 | border-radius: $radius-medium; 13 | 14 | &::placeholder { 15 | color: $grey; 16 | font-size: $size-7; 17 | } 18 | 19 | &:focus { 20 | outline: 0 !important; 21 | box-shadow: none !important; 22 | border-color: $green; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Bulma/RJSFFormFieldAdapter/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with RJSFFormFieldAdapter. 5 | */ 6 | export default defineMessages({ 7 | uploadFilePrompt: { 8 | id: "Form.textUpload.prompt", 9 | defaultMessage: "Drop GeoJSON file here or click to select file", 10 | }, 11 | 12 | readOnlyFile: { 13 | id: "Form.textUpload.readonly", 14 | defaultMessage: "Existing file will be used", 15 | }, 16 | 17 | addPriorityRuleLabel: { 18 | id: "Form.controls.addPriorityRule.label", 19 | defaultMessage: "Add a Rule", 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/TaskPane/ChallengeShareControls/ChallengeShareControls.scss: -------------------------------------------------------------------------------- 1 | .challenge-share-controls { 2 | display: flex; 3 | flex-direction: row; 4 | align-items: center; 5 | justify-content: flex-start; 6 | flex-wrap: wrap; 7 | overflow: hidden; 8 | 9 | .share-control { 10 | margin-right: 15px; 11 | 12 | &:hover { 13 | cursor: pointer; 14 | } 15 | 16 | .SocialMediaShareButton:focus { 17 | outline: 0; 18 | } 19 | } 20 | 21 | &.is-minimized { 22 | flex-direction: column; 23 | 24 | .share-control { 25 | margin-right: 0; 26 | margin-bottom: 15px; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskLocationMap/TaskLocationMap.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .task-location-map { 4 | position: relative; 5 | border: 2px solid $grey-lighter; 6 | border-radius: $radius; 7 | 8 | &__extent-map { 9 | position: absolute; 10 | top: -20px; 11 | right: -15px; 12 | height:90px; 13 | width: 90px; 14 | box-shadow: -4px 5px 20px -6px $grey-darkest; 15 | z-index: $intralayer-bump; 16 | border: 2px solid $grey-lighter; 17 | border-radius: $radius; 18 | 19 | .leaflet-container { 20 | height: 100%; 21 | border-radius: 0 0 $radius-medium 0; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ProjectListBlock/ProjectListBlock.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .admin .project-list-block { 4 | .grid-block__header__title-row { 5 | } 6 | 7 | .grid-block__header__title-row__title { 8 | h2.subtitle { 9 | margin-top: 8px; 10 | margin-right: 30px; 11 | } 12 | } 13 | 14 | &__searchbox.search-box { 15 | max-width: 250px; 16 | margin: 10px 20px 10px 0; 17 | background-color: $white; 18 | 19 | .control-wrapper { 20 | width: 100%; 21 | } 22 | } 23 | 24 | .project-list { 25 | display: flex; 26 | justify-content: center; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/ConfirmAction/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ConfirmAction. 5 | */ 6 | export default defineMessages({ 7 | title: { 8 | id: "ConfirmAction.title", 9 | defaultMessage: "Please Confirm", 10 | }, 11 | 12 | prompt: { 13 | id: "ConfirmAction.prompt", 14 | defaultMessage: "Are you sure? This cannot be undone.", 15 | }, 16 | 17 | cancel: { 18 | id: "ConfirmAction.cancel", 19 | defaultMessage: "Cancel", 20 | }, 21 | 22 | proceed: { 23 | id: "ConfirmAction.proceed", 24 | defaultMessage: "Proceed", 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeKeywords/ChallengeKeywords.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ChallengeKeywords from './ChallengeKeywords' 3 | 4 | let basicProps = null 5 | 6 | beforeEach(() => { 7 | basicProps = { 8 | challenge: { 9 | id: 123, 10 | tags: ["foo", "bar", "baz"], 11 | } 12 | } 13 | }) 14 | 15 | test("renders the challenge tags", () => { 16 | const wrapper = shallow( 17 | 18 | ) 19 | 20 | expect( 21 | wrapper.find('.challenge-keywords .tag').length 22 | ).toBe(basicProps.challenge.tags.length) 23 | 24 | expect(wrapper).toMatchSnapshot() 25 | }) 26 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/CompletionProgressBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with CompletionProgressBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.CompletionProgressBlock.label", 9 | defaultMessage: "Completion Progress", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.CompletionProgressBlock.title", 14 | defaultMessage: "Completion Progress", 15 | }, 16 | 17 | noTasks: { 18 | id: "GridBlocks.CompletionProgressBlock.noTasks", 19 | defaultMessage: "Challenge has no tasks", 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/components/SvgSymbol/SvgSymbol.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import SvgSymbol from './SvgSymbol' 3 | 4 | test('it renders a reference to the given svg', () => { 5 | const wrapper = shallow( 6 | 7 | ) 8 | 9 | expect(wrapper.find('use[xlinkHref="#foo"]').exists()).toBe(true) 10 | expect(wrapper).toMatchSnapshot() 11 | }) 12 | 13 | test('it includes the given viewbox', () => { 14 | const wrapper = shallow( 15 | 16 | ) 17 | 18 | expect(wrapper.find('svg[viewBox="0 0 20 20"]').exists()).toBe(true) 19 | expect(wrapper).toMatchSnapshot() 20 | }) 21 | -------------------------------------------------------------------------------- /src/interactions/Project/AsManageableProject.js: -------------------------------------------------------------------------------- 1 | import _filter from 'lodash/filter' 2 | import _isObject from 'lodash/isObject' 3 | 4 | /** 5 | * AsManageable adds functionality to a Project related to management. 6 | */ 7 | export class AsManageableProject { 8 | constructor(project) { 9 | Object.assign(this, project) 10 | } 11 | 12 | childChallenges(challenges) { 13 | return _filter(challenges, challenge => 14 | _isObject(challenge.parent) ? challenge.parent.id === this.id : 15 | challenge.parent === this.id 16 | ) 17 | } 18 | } 19 | 20 | export default project => new AsManageableProject(project) 21 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/Dashboard/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with Dashboard 5 | */ 6 | export default defineMessages({ 7 | renameConfigurationTooltip: { 8 | id: "Dashboard.controls.renameConfiguration.tooltip", 9 | defaultMessage: "Rename layout", 10 | }, 11 | 12 | addConfigurationTooltip: { 13 | id: "Dashboard.controls.addConfiguration.tooltip", 14 | defaultMessage: "Add new layout", 15 | }, 16 | 17 | deleteConfigurationTooltip: { 18 | id: "Dashboard.controls.deleteConfiguration.tooltip", 19 | defaultMessage: "Delete layout", 20 | }, 21 | }) 22 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeZoom/ChallengeZoom.js: -------------------------------------------------------------------------------- 1 | import _range from 'lodash/range' 2 | 3 | /** 4 | * Constants related to zoom levels. These are based on 5 | * OpenStreetMap zoom levels. 6 | * 7 | * @see See https://wiki.openstreetmap.org/wiki/Zoom_levels 8 | * 9 | * @author [Neil Rotstan](https://github.com/nrotstan) 10 | */ 11 | 12 | /** Minimum possible zoom */ 13 | export const MIN_ZOOM = 0 14 | 15 | /** Maximum possible zoom */ 16 | export const MAX_ZOOM = 19 17 | 18 | /** Default zoom level */ 19 | export const DEFAULT_ZOOM = 13 20 | 21 | /** Array of all zoom levels */ 22 | export const ZOOM_LEVELS = Object.freeze(_range(MIN_ZOOM, MAX_ZOOM + 1)) 23 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectsDashboard/ProjectsDashboard.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .admin__manage.projects-dashboard { 4 | &__dashboard-controls { 5 | display: flex; 6 | justify-content: flex-end; 7 | } 8 | 9 | .projects-dashboard__no-projects { 10 | margin: 40px 15px 15px 15px; 11 | padding: 15px; 12 | color: $primary; 13 | background-color: $white; 14 | font-size: $size-5; 15 | border-radius: $radius-medium; 16 | text-align: center; 17 | } 18 | 19 | .heading-name { 20 | transform-origin: left; 21 | transform: rotate(0deg) translate(0px, 0px); 22 | transition: transform 0.5s; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/DashboardFilterToggle/DashboardFilterToggle.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | const DashboardFilterToggle = (filterType, filterName) => { 4 | return class extends Component { 5 | render() { 6 | return ( 7 |
8 | this.props.toggleEntityFilter(filterName)} /> {this.props.filterToggleLabel} 11 |
12 | ) 13 | } 14 | } 15 | } 16 | 17 | export default DashboardFilterToggle 18 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/StepNavigation/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with StepNavigation 5 | */ 6 | export default defineMessages({ 7 | cancel: { 8 | id: "StepNavigation.controls.cancel.label", 9 | defaultMessage: "Cancel", 10 | }, 11 | 12 | next: { 13 | id: "StepNavigation.controls.next.label", 14 | defaultMessage: "Next", 15 | }, 16 | 17 | prev: { 18 | id: "StepNavigation.controls.prev.label", 19 | defaultMessage: "Prev", 20 | }, 21 | 22 | finish: { 23 | id: "StepNavigation.controls.finish.label", 24 | defaultMessage: "Finish", 25 | }, 26 | }) 27 | 28 | -------------------------------------------------------------------------------- /src/components/HOCs/WithErrors/WithErrors.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { addError, removeError, clearErrors } from '../../../services/Error/Error' 3 | 4 | export const mapStateToProps = (state, ownProps) => { 5 | return ({errors: state.currentErrors}) 6 | } 7 | 8 | export const mapDispatchToProps = dispatch => { 9 | return { 10 | addError: error => dispatch(addError(error)), 11 | removeError: error => dispatch(removeError(error)), 12 | clearErrors: () => dispatch(clearErrors()), 13 | } 14 | } 15 | 16 | const WithErrors = 17 | WrappedComponent => connect(mapStateToProps, mapDispatchToProps)(WrappedComponent) 18 | 19 | export default WithErrors 20 | -------------------------------------------------------------------------------- /src/components/ProjectLeaderboard/ProjectLeaderboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Leaderboard from '../Leaderboard/Leaderboard' 3 | import WithProject from '../HOCs/WithProject/WithProject' 4 | import _get from 'lodash/get' 5 | 6 | export class ProjectLeaderboard extends Component { 7 | render() { 8 | return 12 | } 13 | } 14 | 15 | export default WithProject(ProjectLeaderboard) 16 | -------------------------------------------------------------------------------- /src/components/HOCs/WithTaskCenterPoint/WithTaskCenterPoint.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import AsMappableTask from '../../../interactions/Task/AsMappableTask' 4 | 5 | export default function(WrappedComponent) { 6 | class WithTaskCenterPoint extends Component { 7 | render() { 8 | const mappableTask = AsMappableTask(this.props.task) 9 | return 11 | } 12 | } 13 | 14 | WithTaskCenterPoint.propTypes = { 15 | task: PropTypes.object.isRequired, 16 | } 17 | 18 | return WithTaskCenterPoint 19 | } 20 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeLocation/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeLocation 5 | */ 6 | export default defineMessages({ 7 | nearMe: { 8 | id: "Challenge.location.nearMe", 9 | defaultMessage: "Near Me", 10 | }, 11 | withinMapBounds: { 12 | id: "Challenge.location.withinMapBounds", 13 | defaultMessage: "Within Map Bounds", 14 | }, 15 | intersectingMapBounds: { 16 | id: "Challenge.location.intersectingMapBounds", 17 | defaultMessage: "Intersecting Map Bounds", 18 | }, 19 | any: { 20 | id: "Challenge.location.any", 21 | defaultMessage: "Anywhere", 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /src/components/ChallengePane/ChallengeResultList/SortChallengesSelector.scss: -------------------------------------------------------------------------------- 1 | @import 'theme.scss'; 2 | 3 | .sort-challenges-selector { 4 | .dropdown-trigger { 5 | line-height: 24px; 6 | 7 | .button { 8 | min-width: 110px; 9 | } 10 | 11 | .button.is-outlined { 12 | @include invert-on-hover($grey-light, $white); 13 | font-weight: $weight-normal; 14 | 15 | .dropdown-indicator { 16 | @include arrow($grey-light); 17 | height: 10px; 18 | width: 10px; 19 | position: relative; 20 | margin-left: 0.5em; 21 | bottom: 2px; 22 | } 23 | } 24 | } 25 | 26 | .dropdown-item { 27 | font-weight: $weight-normal; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/components/MobileFilterMenu/MobileFilterMenu.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .mobile-filter-menu { 4 | padding: 8px 10px; 5 | 6 | .dropdown-trigger { 7 | margin-top: 3px; 8 | } 9 | 10 | &__label { 11 | position: relative; 12 | color: $blue; 13 | text-transform: uppercase; 14 | font-size: $size-8; 15 | font-weight: $weight-medium; 16 | letter-spacing: $letter-spacing-medium; 17 | } 18 | 19 | aside.menu { 20 | width: 250px; 21 | max-height: 65vh; 22 | overflow-y: auto; 23 | 24 | .challenge_filter_subnav__other-keywords { 25 | padding: 6px 0.75em 6px 1.25em; 26 | 27 | .search-box button.is-clear { 28 | height: 20px; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/components/EnhancedMap/LayerToggle/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with LayerToggle 5 | */ 6 | export default defineMessages({ 7 | showTaskFeaturesLabel: { 8 | id: "LayerToggle.controls.showTaskFeatures.label", 9 | defaultMessage: "Task Features" 10 | }, 11 | 12 | showMapillaryLabel: { 13 | id: "LayerToggle.controls.showMapillary.label", 14 | defaultMessage: "Mapillary" 15 | }, 16 | 17 | imageCount: { 18 | id: "LayerToggle.imageCount", 19 | defaultMessage: "({count, plural, =0 {no images} other {# images}})" 20 | }, 21 | 22 | loading: { 23 | id: "LayerToggle.loading", 24 | defaultMessage: "(loading...)" 25 | }, 26 | }) 27 | -------------------------------------------------------------------------------- /src/components/CommentList/CommentCountBadge/CommentCountBadge.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import CommentCountBadge from './CommentCountBadge' 3 | 4 | test('it renders a badge with the count of the given comments', () => { 5 | const wrapper = shallow( 6 | 7 | ) 8 | 9 | expect(wrapper.find('.badge[data-badge=3]').exists()).toBe(true) 10 | expect(wrapper).toMatchSnapshot() 11 | }) 12 | 13 | test('it renders a badge with is-empty class for empty comments', () => { 14 | const wrapper = shallow( 15 | 16 | ) 17 | 18 | expect(wrapper.find('.badge.is-empty[data-badge=0]').exists()).toBe(true) 19 | expect(wrapper).toMatchSnapshot() 20 | }) 21 | -------------------------------------------------------------------------------- /src/components/ChallengeLeaderboard/ChallengeLeaderboard.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import Leaderboard from '../Leaderboard/Leaderboard' 3 | import WithChallenge from '../HOCs/WithChallenge/WithChallenge' 4 | import _get from 'lodash/get' 5 | 6 | export class ChallengeLeaderboard extends Component { 7 | render() { 8 | return 13 | } 14 | } 15 | 16 | export default WithChallenge(ChallengeLeaderboard) 17 | -------------------------------------------------------------------------------- /src/components/ChallengePane/ChallengeFilterSubnav/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeFilterSubnav 5 | */ 6 | export default defineMessages({ 7 | difficultyLabel: { 8 | id: 'ChallengeFilterSubnav.filter.difficulty.label', 9 | defaultMessage: 'Difficulty', 10 | }, 11 | keywordLabel: { 12 | id: 'ChallengeFilterSubnav.filter.keyword.label', 13 | defaultMessage: 'Work on', 14 | }, 15 | locationLabel: { 16 | id: 'ChallengeFilterSubnav.filter.location.label', 17 | defaultMessage: 'Location', 18 | }, 19 | searchLabel: { 20 | id: 'ChallengeFilterSubnav.filter.search.label', 21 | defaultMessage: 'Search for a challenge...', 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskCompletionStep2/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskCompletionStep2 5 | */ 6 | export default defineMessages({ 7 | fixed: { 8 | id: 'ActiveTask.controls.fixed.label', 9 | defaultMessage: "I fixed it!", 10 | }, 11 | 12 | notFixed: { 13 | id: 'ActiveTask.controls.notFixed.label', 14 | defaultMessage: "Too difficult / Couldn't see", 15 | }, 16 | 17 | alreadyFixed: { 18 | id: 'ActiveTask.controls.aleadyFixed.label', 19 | defaultMessage: "Already fixed", 20 | }, 21 | 22 | cancelEditing: { 23 | id: 'ActiveTask.controls.cancelEditing.label', 24 | defaultMessage: "Go Back", 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /src/services/User/Locale/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with Locale 5 | */ 6 | export default defineMessages({ 7 | 'en-US': { 8 | id: "Locale.en-US.label", 9 | defaultMessage: "en-US (U.S. English)", 10 | }, 11 | es: { 12 | id: "Locale.es.label", 13 | defaultMessage: "es (Español)", 14 | }, 15 | de: { 16 | id: "Locale.de.label", 17 | defaultMessage: "de (Deutsch)", 18 | }, 19 | fr: { 20 | id: "Locale.fr.label", 21 | defaultMessage: "fr (Français)", 22 | }, 23 | af: { 24 | id: "Locale.af.label", 25 | defaultMessage: "af (Afrikaans)", 26 | }, 27 | ja: { 28 | id: "Locale.ja.label", 29 | defaultMessage: "ja (日本語)", 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /features/pages/OpenStreetMap.js: -------------------------------------------------------------------------------- 1 | import { Page } from './Page' 2 | 3 | class OpenStreetMap extends Page { 4 | get username() { 5 | return browser.element("input#username") 6 | } 7 | 8 | get password() { 9 | return browser.element("input#password") 10 | } 11 | 12 | get loginButton() { 13 | return browser.element("#login_form input[type=submit]") 14 | } 15 | 16 | get authorizeButton() { 17 | return browser.element(".oauth-authorize form input[type=submit]") 18 | } 19 | 20 | waitForAuthorizationForm() { 21 | browser.waitForVisible(".oauth-authorize form", 10000) 22 | } 23 | 24 | open() { 25 | browser.url('https://www.openstreetmap.org/login') 26 | browser.waitForVisible("#login_form", 10000) 27 | } 28 | } 29 | 30 | export default new OpenStreetMap() 31 | -------------------------------------------------------------------------------- /features/steps/HomePageSteps.js: -------------------------------------------------------------------------------- 1 | import HomePage from '../pages/HomePage' 2 | 3 | export default function() { 4 | this.Given(/^(\w+) visits (MapRoulette|the site|the home page)$/, function(username, pageName) { 5 | HomePage.open() 6 | }) 7 | 8 | this.Given(/^(\w+) is browsing MapRoulette$/, function(username) { 9 | if (!HomePage.appIsVisible()) { 10 | HomePage.open() 11 | } 12 | 13 | HomePage.waitForKnownLoginStatus() 14 | HomePage.getStarted.click() 15 | }) 16 | 17 | this.Given(/^(\w+) opens the Account nav menu$/, function(username) { 18 | HomePage.waitForKnownLoginStatus() 19 | HomePage.accountNavMenu.click() 20 | }) 21 | 22 | this.Given(/^(\w+) clicks Get Started on the home page$/, function(user) { 23 | HomePage.getStarted.click() 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/services/Task/TaskLoadMethod/TaskLoadMethod.js: -------------------------------------------------------------------------------- 1 | import _fromPairs from 'lodash/fromPairs' 2 | import _map from 'lodash/map' 3 | import messages from './Messages' 4 | 5 | /** Load tasks randomly within challenge */ 6 | export const RANDOM_LOAD_METHOD = 'random' 7 | 8 | /** Load tasks by proximity within challenge */ 9 | export const PROXIMITY_LOAD_METHOD = 'proximity' 10 | 11 | export const TaskLoadMethod = Object.freeze({ 12 | random: RANDOM_LOAD_METHOD, 13 | proximity: PROXIMITY_LOAD_METHOD, 14 | }) 15 | 16 | /** 17 | * Returns an object mapping status values to raw internationalized 18 | * messages suitable for use with FormattedMessage or formatMessage. 19 | */ 20 | export const messagesByLoadMethod = _fromPairs( 21 | _map(messages, (message, key) => [TaskLoadMethod[key], message]) 22 | ) 23 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/Dashboard/Dashboard.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .dashboard { 4 | position: relative; 5 | background-color: $grey-lightest-more; 6 | border-radius: $radius-medium; 7 | margin-top: -20px; 8 | 9 | &__tab-row { 10 | background-color: $white; 11 | display: flex; 12 | justify-content: space-between; 13 | 14 | &__tabs { 15 | display: flex; 16 | justify-content: flex-start; 17 | } 18 | 19 | &__controls { 20 | display: flex; 21 | align-items: center; 22 | 23 | .button { 24 | margin-left: 15px; 25 | } 26 | } 27 | } 28 | } 29 | 30 | .admin .dashboard .tabs ul { 31 | border-bottom-style: none; 32 | 33 | li { 34 | &.is-active { 35 | margin-bottom: 0; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/KeywordAutosuggestInput/KeywordAutosuggestInput.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins.scss'; 2 | 3 | .keyword-autosuggest-input { 4 | @include basic-form-element-styling(); 5 | 6 | > span { // wrapper around react-tagsinput 7 | display: flex; 8 | } 9 | 10 | .react-tagsinput-tag { 11 | @include tag(); 12 | display: inline-flex; 13 | } 14 | 15 | .autosuggest-text-box__input-wrapper { 16 | input { 17 | border: none; 18 | box-shadow: none; 19 | } 20 | } 21 | 22 | .dropdown-menu { 23 | margin-bottom: 10px; 24 | 25 | .dropdown-content { 26 | .new-keyword { 27 | padding-bottom: 11px; 28 | border-bottom: 2px solid $grey-lightest; 29 | margin-bottom: 5px; 30 | font-style: italic; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/interactions/Challenge/AsMappableChallenge.js: -------------------------------------------------------------------------------- 1 | import _isFinite from 'lodash/isFinite' 2 | import { basemapLayerSource } 3 | from '../../services/VisibleLayer/LayerSources' 4 | 5 | /** 6 | * AsMappableChallenge adds functionality to a Challenge related to mapping. 7 | */ 8 | export class AsMappableChallenge { 9 | constructor(challenge) { 10 | Object.assign(this, challenge) 11 | } 12 | 13 | defaultLayerSource() { 14 | if (!_isFinite(this.id)) { 15 | return null 16 | } 17 | 18 | return basemapLayerSource(this.defaultBasemap, 19 | this.defaultBasemapId, 20 | this.customBasemap, 21 | `challenge_${this.id}`) 22 | } 23 | } 24 | 25 | export default challenge => new AsMappableChallenge(challenge) 26 | -------------------------------------------------------------------------------- /src/components/CommentList/CommentCountBadge/CommentCountBadge.scss: -------------------------------------------------------------------------------- 1 | @import '../../../variables.scss'; 2 | 3 | .comment-count-badge { 4 | display: inline-block; 5 | 6 | .badge.is-badge-outlined[data-badge] { 7 | font-size: 22px; // badge size and positioning affected by font size 8 | margin-left: 4px; 9 | 10 | svg { 11 | height: 19px; 12 | width: auto; 13 | fill: $grey; 14 | } 15 | } 16 | 17 | .badge.is-badge-outlined[data-badge]::after { 18 | font-size: $size-9; 19 | padding: 0.25rem; 20 | color: $white; 21 | background-color: $coral; 22 | border: none; 23 | left: calc(100% - ( 1.2rem / 2 ) - 1px) // move slightly to the left 24 | } 25 | 26 | .badge.is-badge-outlined.is-empty[data-badge]::after { 27 | background-color: $grey-light; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/services/Editor/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with Editor 5 | */ 6 | export default defineMessages({ 7 | none: { 8 | id: "Editor.none.label", 9 | defaultMessage: "None" 10 | }, 11 | id: { 12 | id: "Editor.id.label", 13 | defaultMessage: "Edit in iD (web editor)" 14 | }, 15 | josm: { 16 | id: "Editor.josm.label", 17 | defaultMessage: "Edit in JOSM" 18 | }, 19 | josmLayer: { 20 | id: "Editor.josmLayer.label", 21 | defaultMessage: "Edit in new JOSM layer" 22 | }, 23 | josmFeatures: { 24 | id: "Editor.josmFeatures.label", 25 | defaultMessage: "Edit just features in JOSM" 26 | }, 27 | level0: { 28 | id: "Editor.level0.label", 29 | defaultMessage: "Edit in Level0" 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /src/services/VisibleLayer/VisibleLayer.js: -------------------------------------------------------------------------------- 1 | // redux actions 2 | const CHANGE_VISIBLE_LAYER = 'ChangeVisibleLayer' 3 | 4 | // redux action creators 5 | 6 | /** 7 | * Set the given map tile layer as the current visible tile layer in the redux 8 | * store. 9 | * 10 | * @param {string} layerId - the layerId of the layer to set. It must correspond 11 | * to a valid layer source layerId. 12 | * 13 | * @see See VisibleLayer/LayerSources 14 | */ 15 | export const changeVisibleLayer = function(layerId) { 16 | return { 17 | type: CHANGE_VISIBLE_LAYER, 18 | layerId, 19 | } 20 | } 21 | 22 | // redux reducers 23 | export const visibleLayer = function(state=null, action) { 24 | if (action.type === CHANGE_VISIBLE_LAYER) { 25 | return {id: action.layerId} 26 | } 27 | else { 28 | return state 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Navbar/AccountNavItem/AccountNavItem.scss: -------------------------------------------------------------------------------- 1 | @import '../../../theme.scss'; 2 | 3 | .navbar__account-nav-item { 4 | .dropdown-trigger { 5 | display: flex; 6 | justify-content: flex-start; 7 | align-items: center; 8 | 9 | &:hover { 10 | cursor: pointer; 11 | } 12 | } 13 | 14 | .dropdown-menu { 15 | z-index: $layer-dropdown + $intralayer-bump; 16 | } 17 | 18 | &__avatar { 19 | margin-right: 10px; 20 | 21 | .circular-image { 22 | @include circular-image(); 23 | } 24 | } 25 | 26 | &__username { 27 | font-weight: $weight-normal; 28 | margin-right: 10px; 29 | } 30 | 31 | &__icon { 32 | @include arrow($white); 33 | transform: rotate(-45deg); 34 | transform-origin: center; 35 | transition: transform 0.25s; 36 | position: static; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeAnalysisTable/ChallengeAnalysisTable.scss: -------------------------------------------------------------------------------- 1 | @import 'theme.scss'; 2 | 3 | .challenge-analysis-table { 4 | @include styled-react-table() 5 | margin-bottom: 40px; 6 | 7 | &__column-controls { 8 | font-size: $size-5; 9 | margin-bottom: 15px; 10 | 11 | input { 12 | font-size: $size-5; 13 | } 14 | } 15 | 16 | .completed-column { 17 | text-align: center; 18 | 19 | .cell-icon { 20 | fill: $green; 21 | } 22 | } 23 | 24 | .challenge-task-progress { 25 | margin-bottom: 0; 26 | } 27 | 28 | .rt-td.challenge-actions-column { 29 | overflow: inherit; 30 | text-align: right; 31 | 32 | .challenge-actions-column__action-label { 33 | display: flex; 34 | } 35 | 36 | .menu-wrapper { 37 | text-align: left; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/components/HOCs/WithStatus/__snapshots__/WithStatus.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`checkingLoginStatus returns false if no status 1`] = ` 4 | Object { 5 | "checkingLoginStatus": false, 6 | "fetchingChallenges": Array [], 7 | } 8 | `; 9 | 10 | exports[`fetchingChallenges returns empty array if no status 1`] = ` 11 | Object { 12 | "checkingLoginStatus": false, 13 | "fetchingChallenges": Array [], 14 | } 15 | `; 16 | 17 | exports[`mapStateToProps maps checkingLoginStatus 1`] = ` 18 | Object { 19 | "checkingLoginStatus": true, 20 | "fetchingChallenges": Array [], 21 | } 22 | `; 23 | 24 | exports[`mapStateToProps maps fetchingChallenges 1`] = ` 25 | Object { 26 | "checkingLoginStatus": false, 27 | "fetchingChallenges": Array [ 28 | "fetch123", 29 | "fetch456", 30 | ], 31 | } 32 | `; 33 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeStatus/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeStatus. 5 | */ 6 | export default defineMessages({ 7 | none: { 8 | id: "Challenge.status.none", 9 | defaultMessage: "Not Applicable" 10 | }, 11 | building: { 12 | id: "Challenge.status.building", 13 | defaultMessage: "Building" 14 | }, 15 | failed: { 16 | id: "Challenge.status.failed", 17 | defaultMessage: "Failed" 18 | }, 19 | ready: { 20 | id: "Challenge.status.ready", 21 | defaultMessage: "Ready" 22 | }, 23 | partiallyLoaded: { 24 | id: "Challenge.status.partiallyLoaded", 25 | defaultMessage: "Partially Loaded" 26 | }, 27 | finished: { 28 | id: "Challenge.status.finished", 29 | defaultMessage: "Finished" 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/Navbar/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with Navbar. 5 | */ 6 | export default defineMessages({ 7 | results: { 8 | id: 'Navbar.links.challengeResults', 9 | defaultMessage: "Challenges", 10 | }, 11 | 12 | leaderboard: { 13 | id: 'Navbar.links.leaderboard', 14 | defaultMessage: "Leaderboard", 15 | }, 16 | 17 | adminCreate: { 18 | id: 'Navbar.links.admin', 19 | defaultMessage: "Create", 20 | }, 21 | 22 | help: { 23 | id: 'Navbar.links.help', 24 | defaultMessage: "Help", 25 | }, 26 | 27 | profile: { 28 | id: 'Navbar.links.mobile.userProfile', 29 | defaultMessage: "User Profile", 30 | }, 31 | 32 | signout: { 33 | id: 'Navbar.mobile.links.signout', 34 | defaultMessage: "Sign out", 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/interactions/User/AsMappingUser.js: -------------------------------------------------------------------------------- 1 | import _isFinite from 'lodash/isFinite' 2 | import _isObject from 'lodash/isObject' 3 | import { basemapLayerSource } 4 | from '../../services/VisibleLayer/LayerSources' 5 | 6 | /** 7 | * AsMappingUser adds functionality to a user related to mapping. 8 | */ 9 | export class AsMappingUser { 10 | constructor(user) { 11 | Object.assign(this, user) 12 | } 13 | 14 | defaultLayerSource() { 15 | if (!_isFinite(this.id) || !_isObject(this.settings)) { 16 | return null 17 | } 18 | 19 | return basemapLayerSource(this.settings.defaultBasemap, 20 | this.settings.defaultBasemapId, 21 | this.settings.customBasemap, 22 | `user_${this.id}`) 23 | } 24 | } 25 | 26 | export default user => new AsMappingUser(user) 27 | -------------------------------------------------------------------------------- /src/components/Bulma/TriStateCheckbox.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import _omit from 'lodash/omit' 3 | 4 | /** 5 | * Checkbox that supports `indeterminate` prop, allowing checkbox to be 6 | * checked, unchecked, or indeterminate. 7 | */ 8 | export default class TriStateCheckbox extends Component { 9 | componentDidMount() { 10 | this.el.indeterminate = this.props.indeterminate 11 | } 12 | 13 | componentDidUpdate(prevProps) { 14 | if (prevProps.indeterminate !== this.props.indeterminate) { 15 | this.el.indeterminate = this.props.indeterminate 16 | } 17 | } 18 | 19 | render() { 20 | return ( 21 | this.el = el} /> 23 | ) 24 | } 25 | } 26 | 27 | TriStateCheckbox.defaultProps = { 28 | indeterminate: false, 29 | } 30 | -------------------------------------------------------------------------------- /features/Signin.feature: -------------------------------------------------------------------------------- 1 | Feature: Sign-in Feature 2 | 3 | Users sign in via OAuth by using their OpenStreetMap account and granting 4 | access to the MapRoulette app. 5 | 6 | Scenario: User signs in using OpenStreetMap account 7 | Given mr3testing/mr3testing logs in to OpenStreetMap 8 | And mr3testing is browsing MapRoulette 9 | And mr3testing is signed out from MapRoulette 10 | And mr3testing clicks the Sign In nav link 11 | And mr3testing authorizes the MapRoulette app on OpenStreetMap 12 | Then mr3testing should be signed in to MapRoulette 13 | 14 | Scenario: User signs out through the Account nav menu 15 | Given mr3testing is browsing MapRoulette 16 | And mr3testing/mr3testing is signed in to MapRoulette 17 | And mr3testing clicks Sign Out on the Account nav menu 18 | Then mr3testing should be signed out from MapRoulette 19 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeBasemap/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeBasemap. 5 | */ 6 | export default defineMessages({ 7 | none: { 8 | id: "Challenge.basemap.none", 9 | defaultMessage: "None", 10 | }, 11 | noneChallengeOwner: { 12 | id: "Admin.Challenge.basemap.none", 13 | defaultMessage: "User Default", 14 | }, 15 | openStreetMap: { 16 | id: "Challenge.basemap.openStreetMap", 17 | defaultMessage: "OpenStreetMap", 18 | }, 19 | openCycleMap: { 20 | id: "Challenge.basemap.openCycleMap", 21 | defaultMessage: "OpenCycleMap", 22 | }, 23 | bing: { 24 | id: "Challenge.basemap.bing", 25 | defaultMessage: "Bing", 26 | }, 27 | custom: { 28 | id: "Challenge.basemap.custom", 29 | defaultMessage: "Custom", 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /src/components/EnhancedMap/LayerToggle/LayerToggle.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .layer-toggle.dropdown { 4 | position: absolute; 5 | z-index: $layer-map-control + $intralayer-bump; 6 | top: 10px; 7 | right: 10px; 8 | 9 | .dropdown-trigger > button.button:focus, .dropdown-trigger > button.button:hover { 10 | border-color: $grey-lighter; 11 | background-color: $grey-lightest-more; 12 | } 13 | 14 | .layer-toggle__icon { 15 | fill: $green; 16 | width: 30px; 17 | height: auto; 18 | stroke-width: 2px; 19 | } 20 | 21 | .layer-toggle__option-controls { 22 | div.checkbox { 23 | font-size: $size-7; 24 | padding-left: 1rem; 25 | color: $grey-dark; 26 | 27 | &:hover { 28 | color: $grey-dark; 29 | } 30 | 31 | label { 32 | padding-left: 10px; 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/MobileNotSupported/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with MobileNotSupported 5 | */ 6 | export default defineMessages({ 7 | header: { 8 | id: "MobileNotSupported.header", 9 | defaultMessage: "Please Visit on your Computer" 10 | }, 11 | 12 | message: { 13 | id: "MobileNotSupported.message", 14 | defaultMessage: "Sorry, MapRoulette does not currently support mobile devices." 15 | }, 16 | 17 | pageMessage: { 18 | id: "MobileNotSupported.pageMessage", 19 | defaultMessage: "Sorry, this page is not yet compatible with mobile devices and smaller screens." 20 | }, 21 | 22 | widenDisplay: { 23 | id: "MobileNotSupported.widenDisplay", 24 | defaultMessage: "If using a computer, please widen your window or use a larger display." 25 | }, 26 | }) 27 | 28 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeTaskMap/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeTaskMap 5 | */ 6 | export default defineMessages({ 7 | clusterTasksLabel: { 8 | id: "Admin.ChallengeTaskMap.controls.clusterTasks.label", 9 | defaultMessage: "Group Tasks", 10 | }, 11 | 12 | reviewTaskLabel: { 13 | id: "Admin.ChallengeTaskMap.controls.reviewTask.label", 14 | defaultMessage: "Review Task", 15 | }, 16 | 17 | editTaskLabel: { 18 | id: "Admin.ChallengeTaskMap.controls.editTask.label", 19 | defaultMessage: "Edit Task", 20 | }, 21 | 22 | nameLabel: { 23 | id: "Admin.Task.fields.name.label", 24 | defaultMessage: "Task:", 25 | }, 26 | 27 | statusLabel: { 28 | id: "Admin.Task.fields.status.label", 29 | defaultMessage: "Status:", 30 | }, 31 | }) 32 | -------------------------------------------------------------------------------- /src/services/Activity/ActivityItemTypes/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ActivityItemType. 5 | */ 6 | export default defineMessages({ 7 | project: { 8 | id: "Activity.item.project", 9 | defaultMessage: "Project" 10 | }, 11 | challenge: { 12 | id: "Activity.item.challenge", 13 | defaultMessage: "Challenge" 14 | }, 15 | task: { 16 | id: "Activity.item.task", 17 | defaultMessage: "Task" 18 | }, 19 | tag: { 20 | id: "Activity.item.tag", 21 | defaultMessage: "Tag" 22 | }, 23 | survey: { 24 | id: "Activity.item.survey", 25 | defaultMessage: "Survey" 26 | }, 27 | user: { 28 | id: "Activity.item.user", 29 | defaultMessage: "User" 30 | }, 31 | group: { 32 | id: "Activity.item.group", 33 | defaultMessage: "Group" 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/ChallengePane/ChallengePane.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ChallengePane } from './ChallengePane' 3 | import { ChallengeDifficulty } 4 | from '../../services/Challenge/ChallengeDifficulty/ChallengeDifficulty' 5 | 6 | let challenge = null 7 | let basicProps = null 8 | 9 | beforeEach(() => { 10 | challenge = { 11 | id: 123, 12 | } 13 | 14 | basicProps = { 15 | user: { 16 | id: 11, 17 | savedChallenges: [], 18 | }, 19 | startChallenge: jest.fn(), 20 | saveChallenge: jest.fn(), 21 | unsaveChallenge: jest.fn(), 22 | intl: {formatMessage: jest.fn()}, 23 | } 24 | }) 25 | 26 | test("renders with props as expected", () => { 27 | const wrapper = shallow( 28 | 29 | ) 30 | 31 | expect(wrapper.find('.challenge-pane').exists()).toBe(true) 32 | expect(wrapper).toMatchSnapshot() 33 | }) 34 | -------------------------------------------------------------------------------- /src/components/HOCs/WithEditor/WithEditor.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { editTask, closeEditor } from '../../../services/Editor/Editor' 3 | 4 | /** 5 | * WithEditor provides an editor prop to its WrappedComponent that contains the 6 | * current open editor (if any) from the redux store, as well as functions for 7 | * initiating and ending editing of a task. 8 | * 9 | * @author [Neil Rotstan](https://github.com/nrotstan) 10 | */ 11 | const WithEditor = 12 | WrappedComponent => connect(mapStateToProps, mapDispatchToProps)(WrappedComponent) 13 | 14 | export const mapStateToProps = state => ({ 15 | editor: state.openEditor, 16 | }) 17 | 18 | export const mapDispatchToProps = dispatch => ({ 19 | editTask: (editor, task, mapBounds) => dispatch(editTask(editor, task, mapBounds)), 20 | closeEditor: () => dispatch(closeEditor()), 21 | }) 22 | 23 | export default WithEditor 24 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ReviewTaskControls/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ReviewTaskControls 5 | */ 6 | export default defineMessages({ 7 | previousTaskLabel: { 8 | id: "Admin.TaskReview.controls.previousTask.label", 9 | defaultMessage: "Prior Task", 10 | }, 11 | nextTaskLabel: { 12 | id: "Admin.TaskReview.controls.nextTask.label", 13 | defaultMessage: "Next Task", 14 | }, 15 | editTaskLabel: { 16 | id: "Admin.TaskReview.controls.editTask.label", 17 | defaultMessage: "Edit Task", 18 | }, 19 | modifyTaskLabel: { 20 | id: "Admin.TaskReview.controls.modifyTask.label", 21 | defaultMessage: "Modify Task Data", 22 | }, 23 | postCommentLabel: { 24 | id: "Admin.TaskReview.controls.postTaskComment.label", 25 | defaultMessage: "Post", 26 | }, 27 | 28 | }) 29 | -------------------------------------------------------------------------------- /src/components/UserProfile/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with UserProfile. 5 | */ 6 | export default defineMessages({ 7 | apiKey: { 8 | id: "UserProfile.apiKey.header", 9 | defaultMessage: "API Key", 10 | }, 11 | 12 | apiKeyCopyLabel: { 13 | id: "UserProfile.apiKey.controls.copy.label", 14 | defaultMessage: "Copy", 15 | }, 16 | 17 | apiKeyResetLabel: { 18 | id: "UserProfile.apiKey.controls.reset.label", 19 | defaultMessage: "Reset", 20 | }, 21 | 22 | overviewTab: { 23 | id: "UserProfile.tabs.overview", 24 | defaultMessage: "Overview", 25 | }, 26 | 27 | activityTab: { 28 | id: "UserProfile.tabs.activity", 29 | defaultMessage: "Activity", 30 | }, 31 | 32 | settingsTab: { 33 | id: "UserProfile.tabs.settings", 34 | defaultMessage: "Settings", 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/services/Task/TaskStatus/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with TaskStatus. 5 | */ 6 | export default defineMessages({ 7 | created: { 8 | id: "Task.status.created", 9 | defaultMessage: "Created" 10 | }, 11 | fixed: { 12 | id: "Task.status.fixed", 13 | defaultMessage: "Fixed" 14 | }, 15 | falsePositive: { 16 | id: "Task.status.falsePositive", 17 | defaultMessage: "Not an Issue" 18 | }, 19 | skipped: { 20 | id: "Task.status.skipped", 21 | defaultMessage: "Skipped" 22 | }, 23 | deleted: { 24 | id: "Task.status.deleted", 25 | defaultMessage: "Deleted" 26 | }, 27 | alreadyFixed: { 28 | id: "Task.status.alreadyFixed", 29 | defaultMessage: "Already Fixed" 30 | }, 31 | tooHard: { 32 | id: "Task.status.tooHard", 33 | defaultMessage: "Too Hard" 34 | }, 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/AdminPane/HOCs/WithWideScreenOption/WithWideScreenOption.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | 3 | /** 4 | * WithWideScreenOption passes down an isWideScreen prop to the wrapped component, 5 | * as well as a setWideScreen function for altering the isWideScreen prop. 6 | * 7 | * @author [Neil Rotstan](https://github.com/nrotstan) 8 | */ 9 | export default function WithWideScreenOption(WrappedComponent, defaultToWide=false) { 10 | return class extends Component { 11 | state = { 12 | isWideScreen: defaultToWide 13 | } 14 | 15 | setWideScreen = (isWide=true) => { 16 | this.setState({isWideScreen: isWide}) 17 | } 18 | 19 | render() { 20 | return 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/components/Bulma/Menu.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import MenuList from './MenuList' 4 | import './Menu.css' 5 | 6 | /** 7 | * Menu renders a Bulma menu with the given menuLists 8 | */ 9 | export default class Menu extends Component { 10 | render() { 11 | const menuLists = this.props.menuLists ? 12 | this.props.menuLists.map(menuConfig => 13 | 17 | ) : this.props.children 18 | 19 | return ( 20 | 21 | ) 22 | } 23 | } 24 | 25 | Menu.propTypes = { 26 | menuLists: PropTypes.arrayOf(PropTypes.object), 27 | } 28 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeType/ChallengeType.js: -------------------------------------------------------------------------------- 1 | import _map from 'lodash/map' 2 | import _fromPairs from 'lodash/fromPairs' 3 | import messages from './Messages' 4 | 5 | // These constants are defined on the server 6 | export const CHALLENGE_TYPE_CHALLENGE = 1 7 | export const CHALLENGE_TYPE_SURVEY = 4 8 | 9 | export const ChallengeType = Object.freeze({ 10 | challenge: CHALLENGE_TYPE_CHALLENGE, 11 | survey: CHALLENGE_TYPE_SURVEY, 12 | }) 13 | 14 | /** 15 | * Returns an object mapping difficulty values to raw internationalized 16 | * messages suitable for use with FormattedMessage or formatMessage. 17 | */ 18 | export const messagesByType = _fromPairs( 19 | _map(messages, (message, key) => [ChallengeType[key], message]) 20 | ) 21 | 22 | /** Returns object containing localized labels */ 23 | export const typeLabels = intl => _fromPairs( 24 | _map(messages, (message, key) => [key, intl.formatMessage(message)]) 25 | ) 26 | -------------------------------------------------------------------------------- /src/components/ChallengeProgress/ChallengeProgress.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ChallengeProgress } from './ChallengeProgress' 3 | import _cloneDeep from 'lodash/cloneDeep' 4 | 5 | let basicProps = null 6 | 7 | beforeEach(() => { 8 | basicProps = { 9 | challenge: { 10 | id: 123, 11 | actions: {total: 5, available: 3, completed: 2} 12 | }, 13 | intl: {formatMessage: jest.fn()}, 14 | } 15 | }) 16 | 17 | test("renders with props as expected", () => { 18 | const wrapper = shallow( 19 | 20 | ) 21 | 22 | expect(wrapper.find('.challenge-task-progress').exists()).toBe(true) 23 | expect(wrapper).toMatchSnapshot() 24 | }) 25 | 26 | test("does not explode with null challenge", () => { 27 | delete basicProps.challenge 28 | const wrapper = shallow( 29 | 30 | ) 31 | 32 | expect(wrapper).toMatchSnapshot() 33 | }) 34 | -------------------------------------------------------------------------------- /src/components/ErrorModal/ErrorModal.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ErrorModal } from './ErrorModal' 3 | 4 | let basicProps = null 5 | 6 | beforeEach(() => { 7 | basicProps = { 8 | errors: [ 9 | { 10 | id: "first-error", 11 | defaultMessage: "First Error" 12 | }, 13 | { 14 | id: "second-error", 15 | defaultMessage: "Second Error" 16 | }, 17 | { 18 | id: "third-error", 19 | defaultMessage: "Third Error" 20 | }, 21 | ], 22 | removeError: jest.fn(), 23 | clearErrors: jest.fn(), 24 | formatMessage: jest.fn((params) => params.defaultMessage), 25 | } 26 | }) 27 | 28 | test('it renders a list of the given errors', () => { 29 | const wrapper = shallow( 30 | 31 | ) 32 | 33 | expect(wrapper.find('li').length).toBe(basicProps.errors.length) 34 | 35 | expect(wrapper).toMatchSnapshot() 36 | }) 37 | -------------------------------------------------------------------------------- /src/interactions/Overpass/AsValidatableOverpass.js: -------------------------------------------------------------------------------- 1 | import messages from './Messages' 2 | 3 | const overpassTurboShortcutRegex = /{{2}[^}]+}{2}/ 4 | 5 | /** 6 | * Provides methods related to validating Overpass queries. 7 | * 8 | * @author [Neil Rotstan](https://github.com/nrotstan) 9 | */ 10 | export class AsValidatableOverpass { 11 | constructor(overpassQuery) { 12 | this.overpassQuery = overpassQuery 13 | } 14 | 15 | /** 16 | * Validate the raw Overpass query. This just looks for a few basic problems, 17 | * such as inclusion of Overpass Turbo query shortcuts (mustache tags). 18 | */ 19 | validate() { 20 | const errors = [] 21 | 22 | if (overpassTurboShortcutRegex.test(this.overpassQuery)) { 23 | errors.push({ 24 | message: messages.noOverpassTurboShortcuts 25 | }) 26 | } 27 | 28 | return errors 29 | } 30 | } 31 | 32 | export default overpassQuery => new AsValidatableOverpass(overpassQuery) 33 | -------------------------------------------------------------------------------- /src/components/Bulma/SvgControl.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import SvgSymbol from '../SvgSymbol/SvgSymbol' 5 | 6 | /** 7 | * SvgControl renders a simple clickable control consisting solely of the SVG 8 | * referenced by the sym prop. 9 | */ 10 | export default class SvgControl extends Component { 11 | render() { 12 | return ( 13 |
15 | 16 | 17 | 18 |
19 | ) 20 | } 21 | } 22 | 23 | SvgControl.propTypes = { 24 | sym: PropTypes.string.isRequired, 25 | viewBox: PropTypes.string, 26 | onClick: PropTypes.func, 27 | } 28 | 29 | SvgControl.defaultProps = { 30 | viewBox: "0 0 20 20", 31 | } 32 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/CalendarHeatmap/CalendarHeatmap.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .calendar-heatmap { 4 | $calendar-bucket-colors: ( 5 | $chart-blue-light, $chart-green, $chart-brown, 6 | $chart-yellow, $chart-orange, $chart-red 7 | ); 8 | 9 | @for $i from 1 through length($calendar-bucket-colors) { 10 | .color-bucket-#{$i - 1} { fill: nth($calendar-bucket-colors, $i); } 11 | } 12 | 13 | .color-empty { 14 | fill: $grey-lightest-more; 15 | } 16 | 17 | .react-calendar-heatmap { 18 | margin-top: 10px; 19 | 20 | text { 21 | font-size: 10px; 22 | fill: #aaa; 23 | } 24 | 25 | .react-calendar-heatmap-small-text { 26 | font-size: 5px; 27 | } 28 | } 29 | 30 | &.vertical { 31 | .react-calendar-heatmap { 32 | width: 140px; 33 | height: 800px; 34 | } 35 | } 36 | 37 | &.high-contrast { 38 | .color-empty { 39 | fill: $white; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ChallengeOverviewBlock/ChallengeOverviewBlock.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .challenge-overview-block { 4 | .grid-block__content { 5 | .columns { 6 | margin: -0.5em 0; 7 | 8 | &:first-child { 9 | margin-top: 0; 10 | } 11 | 12 | &:last-child { 13 | margin-bottom: 0; 14 | } 15 | 16 | .column { 17 | padding: 0.25em; 18 | } 19 | } 20 | 21 | .status-label { 22 | font-weight: $weight-light; 23 | color: $grey; 24 | font-size: $size-6-plus; 25 | } 26 | 27 | .status-value { 28 | color: $grey; 29 | font-size: $size-6-plus; 30 | } 31 | 32 | .challenge-keywords { 33 | margin-top: 15px; 34 | margin-bottom: 20px; 35 | 36 | .tag { 37 | margin-bottom: 5px; 38 | } 39 | } 40 | 41 | .start-challenge-control { 42 | margin-bottom: 20px; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/components/HOCs/WithProjects/WithProjects.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { denormalize } from 'normalizr' 3 | import _compact from 'lodash/compact' 4 | import _map from 'lodash/map' 5 | import _get from 'lodash/get' 6 | import { projectSchema } from '../../../services/Project/Project' 7 | import { fetchProjectChallenges } from '../../../services/Challenge/Challenge' 8 | 9 | export const mapStateToProps = (state, ownProps) => ({ 10 | projects: _compact(_map(_get(state, 'entities.projects', {}), 11 | project => project.id ? 12 | denormalize(project, projectSchema(), state.entities) : 13 | null)) 14 | }) 15 | 16 | export const mapDispatchToProps = dispatch => ({ 17 | fetchProjectChallenges: (projectId) => dispatch(fetchProjectChallenges(projectId)), 18 | }) 19 | 20 | const WithProjects = 21 | WrappedComponent => connect(mapStateToProps, mapDispatchToProps)(WrappedComponent) 22 | 23 | export default WithProjects 24 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/TaskAnalysisTable/TaskAnalysisTable.scss: -------------------------------------------------------------------------------- 1 | @import 'theme.scss'; 2 | 3 | .task-analysis-table { 4 | @include styled-react-table() 5 | 6 | .status-icon { 7 | height: 20px; 8 | width: auto; 9 | vertical-align: middle; 10 | margin-right: 15px; 11 | 12 | &.created { 13 | fill: $status-created-color; 14 | } 15 | 16 | &.fixed { 17 | fill: $status-fixed-color; 18 | } 19 | 20 | &.falsePositive { 21 | fill: $status-falsePositive-color; 22 | } 23 | 24 | &.skipped { 25 | fill: $status-skipped-color; 26 | } 27 | 28 | &.deleted { 29 | fill: $status-deleted-color; 30 | } 31 | 32 | &.alreadyFixed { 33 | fill: $status-alreadyFixed-color; 34 | } 35 | 36 | &.tooHard { 37 | fill: $status-tooHard-color; 38 | } 39 | } 40 | 41 | .ReactTable .rt-table .rt-td.task-analysis-table__selection-option { 42 | padding: 15px 5px; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/SvgSymbol/SvgSymbol.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | /** 5 | * SvgSymbol renders an svg that utilizes a tag to reference the 6 | * _existing_ svg with an id matching the given sym prop on the current page 7 | * (in this app, SVGs are embeded in the page via the the Sprites component). 8 | * 9 | * @see See Sprites for a list of symbols 10 | * 11 | * @author [Neil Rotstan](https://github.com/nrotstan) 12 | */ 13 | export default class SvgSymbol extends Component { 14 | render() { 15 | return ( 16 | 20 | 21 | 22 | ) 23 | } 24 | } 25 | 26 | SvgSymbol.propTypes = { 27 | sym: PropTypes.string.isRequired, 28 | viewBox: PropTypes.string.isRequired, 29 | } 30 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ProjectAboutBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectAboutBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.ProjectAboutBlock.label", 9 | defaultMessage: "About Projects", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.ProjectAboutBlock.title", 14 | defaultMessage: "About Projects", 15 | }, 16 | 17 | content: { 18 | id: "GridBlocks.ProjectAboutBlock.content", 19 | defaultMessage: 20 | `Projects serve as a means of grouping related challenges together. All 21 | challenges must belong to a project. 22 | 23 | You can create as many projects as needed to organize your challenges, and can 24 | invite other MapRoulette users to help manage them with you. 25 | 26 | Projects must be set to visible before any challenges within them will show up 27 | in public browsing or searching.`, 28 | }, 29 | }) 30 | -------------------------------------------------------------------------------- /src/components/HOCs/WithClusteredTasks/WithClusteredTasks.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { fetchClusteredTasks } 3 | from '../../../services/Task/ClusteredTask' 4 | 5 | /** 6 | * WithClusteredTasks provides a clusteredTasks prop containing the current 7 | * clustered task data from the redux store, as well as a fetchClusteredTasks 8 | * function for retrieving the clustered tasks for a given challenge. 9 | * 10 | * @author [Neil Rotstan](https://github.com/nrotstan) 11 | */ 12 | const WithClusteredTasks = WrappedComponent => 13 | connect(mapStateToProps, mapDispatchToProps)(WrappedComponent) 14 | 15 | export const mapStateToProps = state => ({ 16 | clusteredTasks: state.currentClusteredTasks, 17 | }) 18 | 19 | export const mapDispatchToProps = dispatch => ({ 20 | fetchClusteredTasks: async (challengeId, isVirtualChallenge) => 21 | dispatch(fetchClusteredTasks(challengeId, isVirtualChallenge)), 22 | }) 23 | 24 | export default WithClusteredTasks 25 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskMap/TaskMap.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { TaskMap } from './TaskMap' 3 | import { latLng } from 'leaflet' 4 | import _cloneDeep from 'lodash/cloneDeep' 5 | 6 | const propsFixture = { 7 | task: { 8 | id: 2, 9 | geometries: { 10 | features: null 11 | }, 12 | parent: { 13 | defaultZoom: 2, 14 | minZoom: 1, 15 | maxZoom: 0, 16 | } 17 | }, 18 | centerPoint: latLng(0, 0) 19 | } 20 | 21 | let basicProps = null 22 | 23 | beforeEach(() => { 24 | basicProps = _cloneDeep(propsFixture) 25 | basicProps.setTaskMapBounds = jest.fn() 26 | }) 27 | 28 | test("renders with props as expected", () => { 29 | const wrapper = shallow( 30 | 31 | ) 32 | 33 | expect(wrapper.find('.task-map').exists()).toBe(true) 34 | expect(wrapper.find('EnhancedMap').exists()).toBe(true) 35 | expect(wrapper.find('ZoomControl').exists()).toBe(true) 36 | expect(wrapper).toMatchSnapshot() 37 | }) 38 | -------------------------------------------------------------------------------- /src/components/ScreenTooNarrow/ScreenTooNarrow.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .screen-too-narrow { 4 | padding: 30px 20px; 5 | max-width: 100%; 6 | background-color: $app-background; 7 | min-height: $mobile-full-screen-pane; 8 | 9 | &__header { 10 | display: flex; 11 | align-items: center; 12 | margin: 30px; 13 | 14 | svg { 15 | width: 80px; 16 | height: 80px; 17 | fill: $grey-dark; 18 | margin-right: 20px; 19 | } 20 | 21 | h1.title { 22 | margin-bottom: 5px; 23 | } 24 | } 25 | 26 | @media(max-width: $app-mobile-break) { 27 | .screen-too-narrow__header { 28 | margin: 0; 29 | padding: 5px; 30 | 31 | svg { 32 | width: auto; 33 | height: 60px; 34 | } 35 | 36 | h1.title { 37 | font-size: $size-7; 38 | font-weight: $weight-semibold; 39 | } 40 | 41 | .screen-too-narrow__message { 42 | font-size: $size-8; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/services/OSMUser/OSMUser.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Retrieve OpenStreetMap user data for the user with the given OSM user id 3 | * (not the same as a MapRoulette user id). Note that this does not update the 4 | * redux store: it simply resolves the returned promise with the user data. 5 | */ 6 | export const fetchOSMUser = function(osmUserId) { 7 | const osmUserURI = 8 | `https://api.openstreetmap.org/api/0.6/user/${osmUserId}` 9 | 10 | // The OSM api call only returns XML, so extract the display name 11 | return new Promise((resolve, reject) => { 12 | fetch(osmUserURI).then(response => { 13 | if (response.ok) { 14 | response.text().then(xmlData => { 15 | const displayNameMatch = /display_name="([^"]+)"/.exec(xmlData) 16 | resolve({id: osmUserId, displayName: displayNameMatch[1]}) 17 | }) 18 | } 19 | else if (response.status === 404) { // No user found 20 | resolve({}) 21 | } 22 | }).catch(error => reject(error)) 23 | }) 24 | } 25 | -------------------------------------------------------------------------------- /src/interactions/User/AsEndUser.js: -------------------------------------------------------------------------------- 1 | import { GUEST_USER_ID } from '../../services/User/User' 2 | import { GROUP_TYPE_SUPERUSER } 3 | from '../../services/Project/GroupType/GroupType' 4 | import _find from 'lodash/find' 5 | import _isObject from 'lodash/isObject' 6 | import _isNumber from 'lodash/isNumber' 7 | 8 | /** 9 | * Provides basic methods for interacting with users. 10 | */ 11 | export class AsEndUser { 12 | constructor(user) { 13 | this.user = user 14 | } 15 | 16 | /** 17 | * Returns true if the user is logged in, false otherwise. 18 | */ 19 | isLoggedIn() { 20 | return _isObject(this.user) && 21 | _isNumber(this.user.id) && this.user.id !== GUEST_USER_ID 22 | } 23 | 24 | /** 25 | * Returns true if the user is a super user, false otherwise. 26 | */ 27 | isSuperUser() { 28 | return this.isLoggedIn() && 29 | !!_find(this.user.groups, {groupType: GROUP_TYPE_SUPERUSER}) 30 | } 31 | } 32 | 33 | export default user => new AsEndUser(user) 34 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeDashboard/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeDashboard 5 | */ 6 | export default defineMessages({ 7 | startChallengeLabel: { 8 | id: "Admin.Challenge.controls.start.label", 9 | defaultMessage: "Start", 10 | }, 11 | 12 | editChallengeLabel: { 13 | id: "Admin.Challenge.controls.edit.label", 14 | defaultMessage: "Edit", 15 | }, 16 | 17 | moveChallengeLabel: { 18 | id: "Admin.Challenge.controls.move.label", 19 | defaultMessage: "Move", 20 | }, 21 | 22 | noProjects: { 23 | id: "Admin.Challenge.controls.move.none", 24 | defaultMessage: "No permitted projects", 25 | }, 26 | 27 | rebuildChallengeLabel: { 28 | id: "Admin.Challenge.controls.rebuild.label", 29 | defaultMessage: "Rebuild", 30 | }, 31 | 32 | cloneChallengeLabel: { 33 | id: "Admin.Challenge.controls.clone.label", 34 | defaultMessage: "Clone", 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeList/ChallengeList.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .admin__manage__managed-item-list.challenge-list { 4 | .challenge-list-item { 5 | border-top: 2px solid $grey-lightest-less; 6 | margin-top: 0px; 7 | 8 | .challenge-name { 9 | word-break: break-word; 10 | } 11 | } 12 | 13 | .item-entry:not(:last-child) { 14 | margin-bottom: 0; 15 | 16 | &:first-child { 17 | .challenge-list-item { 18 | border-top: none; 19 | 20 | .column { 21 | padding-top: 0; 22 | } 23 | } 24 | } 25 | } 26 | 27 | .challenge-list__controls { 28 | margin-bottom: 20px; 29 | 30 | .button.new-challenge { 31 | box-sizing: content-box; 32 | margin: 20px 0px 12px 0px; 33 | padding: 0px 30px; 34 | font-size: $size-5; 35 | font-weight: $weight-normal; 36 | } 37 | } 38 | 39 | .challenge-list__no-results { 40 | text-align: center; 41 | color: $grey; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ViewTask/ViewTask.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import SyntaxHighlighter, 4 | { registerLanguage } from 'react-syntax-highlighter/light' 5 | import jsonLang from 'react-syntax-highlighter/languages/hljs/json' 6 | import highlightColors from 'react-syntax-highlighter/styles/hljs/github' 7 | import BusySpinner from '../../../BusySpinner/BusySpinner' 8 | 9 | registerLanguage('json', jsonLang); 10 | 11 | export default class ViewTask extends Component { 12 | render() { 13 | if (!this.props.task) { 14 | return 15 | } 16 | 17 | return ( 18 |
19 | 20 | {JSON.stringify(this.props.task.geometries, null, 4)} 21 | 22 |
23 | ) 24 | } 25 | } 26 | 27 | ViewTask.propTypes = { 28 | /** The task to display */ 29 | task: PropTypes.object, 30 | } 31 | -------------------------------------------------------------------------------- /src/components/MobileNotSupported/MobileNotSupported.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .mobile-not-supported { 4 | padding: 30px 20px; 5 | max-width: 100%; 6 | background-color: $app-background; 7 | min-height: $mobile-full-screen-pane; 8 | 9 | &__header { 10 | display: flex; 11 | align-items: center; 12 | margin: 30px; 13 | 14 | svg { 15 | width: 80px; 16 | height: 80px; 17 | fill: $grey-dark; 18 | margin-right: 20px; 19 | } 20 | 21 | h1.title { 22 | margin-bottom: 5px; 23 | } 24 | } 25 | 26 | @media(max-width: $app-mobile-break) { 27 | .mobile-not-supported__header { 28 | margin: 0; 29 | padding: 5px; 30 | 31 | svg { 32 | width: auto; 33 | height: 60px; 34 | } 35 | 36 | h1.title { 37 | font-size: $size-7; 38 | font-weight: $weight-semibold; 39 | } 40 | 41 | .mobile-not-supported__message { 42 | font-size: $size-8; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/ErrorModal/ErrorModal.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | .error-pane { 4 | li { 5 | display: flex; 6 | justify-content: center; 7 | margin-bottom: 15px; 8 | } 9 | 10 | .message { 11 | width: 75%; 12 | max-width: 600px; 13 | border: none; 14 | 15 | .message-header { 16 | padding: 15px; 17 | text-transform: uppercase; 18 | font-size: $size-6; 19 | font-weight: $weight-medium; 20 | letter-spacing: $letter-spacing-medium; 21 | } 22 | 23 | .message-body { 24 | padding: 20px; 25 | border: none; 26 | color: $grey; 27 | font-size: $size-7; 28 | } 29 | 30 | &.is-danger { 31 | color: $coral; 32 | 33 | .message-header { 34 | background-color: $coral; 35 | 36 | button.delete { 37 | background-color: $coral; 38 | border: 2px solid $white; 39 | } 40 | } 41 | 42 | .message-body { 43 | color: $grey; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeKeywords/ChallengeKeywords.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import _compact from 'lodash/compact' 4 | import _map from 'lodash/map' 5 | import _isEmpty from 'lodash/isEmpty' 6 | import './ChallengeKeywords.css' 7 | 8 | /** 9 | * ChallengeKeywords renders the keywords of the given challenge as a tag set. 10 | * 11 | * @author [Neil Rotstan](https://github.com/nrotstan) 12 | */ 13 | export default class ChallengeKeywords extends Component { 14 | render() { 15 | if (!this.props.challenge) { 16 | return null 17 | } 18 | 19 | const keywords = _compact(_map(this.props.challenge.tags, keyword => { 20 | if (_isEmpty(keyword)) { 21 | return null 22 | } 23 | 24 | return {keyword} 25 | })) 26 | 27 | return
{keywords}
28 | } 29 | } 30 | 31 | ChallengeKeywords.propTypes = { 32 | challenge: PropTypes.object, 33 | } 34 | -------------------------------------------------------------------------------- /src/components/BusySpinner/BusySpinner.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import './BusySpinner.css' 5 | 6 | /** 7 | * BusySpinner displays a simple busy spinner. By default it's shown centered 8 | * in a block, but the `inline` prop can be given to display it inline. 9 | * 10 | * @author [Neil Rotstan](https://github.com/nrotstan) 11 | */ 12 | export default class BusySpinner extends Component { 13 | render() { 14 | return ( 15 |
19 |
20 |
21 | ) 22 | } 23 | } 24 | 25 | BusySpinner.propTypes = { 26 | /** display spinner inline, as opposed to a centered block */ 27 | inline: PropTypes.bool, 28 | } 29 | -------------------------------------------------------------------------------- /src/components/AdminPane/HOCs/WithComboSearch/WithComboSearch.js: -------------------------------------------------------------------------------- 1 | import _each from 'lodash/each' 2 | import _toPairs from 'lodash/toPairs' 3 | import WithSearch from '../../../HOCs/WithSearch/WithSearch' 4 | 5 | /** 6 | * WithComboSearch combines together multiple WithSearch HOCs 7 | * for the same wrapped component, allowing a single search query to easily be 8 | * used for multiple discrete searches (e.g. searching both projects and 9 | * challenges simultaneously for a given name entered in a search box). 10 | * 11 | * @param searches {object} containing searchName: searchFunction fields for 12 | * each desired search. 13 | * 14 | * @see See WithSearch 15 | * 16 | * @author [Neil Rotstan](https://github.com/nrotstan) 17 | */ 18 | const WithComboSearch = (WrappedComponent, searches) => { 19 | let Combo = WrappedComponent 20 | 21 | _each(_toPairs(searches), searchConfig => 22 | Combo = WithSearch(Combo, searchConfig[0], searchConfig[1]) 23 | ) 24 | 25 | return Combo 26 | } 27 | 28 | export default WithComboSearch 29 | -------------------------------------------------------------------------------- /src/components/ChallengePane/ChallengeResultList/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeResultList. 5 | */ 6 | export default defineMessages({ 7 | heading: { 8 | id: "Challenge.results.heading", 9 | defaultMessage: "Challenges", 10 | }, 11 | 12 | clearFiltersLabel: { 13 | id: "Challenge.controls.clearFilters.label", 14 | defaultMessage: "Clear Filters", 15 | }, 16 | 17 | noResults: { 18 | id: "Challenge.results.noResults", 19 | defaultMessage: "No Results", 20 | }, 21 | 22 | createVirtualChallenge: { 23 | id: "VirtualChallenge.controls.create.label", 24 | defaultMessage: "Work on {taskCount, number} Mapped Tasks", 25 | }, 26 | 27 | virtualChallengeNameLabel: { 28 | id: "VirtualChallenge.fields.name.label", 29 | defaultMessage: 'Name your "virtual" challenge', 30 | }, 31 | 32 | loadMoreLabel: { 33 | id: "Challenge.controls.loadMore.label", 34 | defaultMessage: "More Results", 35 | }, 36 | }) 37 | -------------------------------------------------------------------------------- /src/components/PageNotFound/PageNotFound.scss: -------------------------------------------------------------------------------- 1 | @import "variables.scss"; 2 | 3 | .page-not-found { 4 | &__message { 5 | position: fixed; 6 | z-index: $layer-top; 7 | top: $navbar-height; 8 | left: 0; 9 | height: $full-screen; 10 | width: 100%; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | align-items: center; 15 | 16 | h1 { 17 | font-size: $size-huge; 18 | line-height: $size-huge; 19 | margin-bottom: 30px; 20 | color: $grey-darkest; 21 | } 22 | 23 | &__icon { 24 | font-size: $size-1; 25 | } 26 | 27 | h2 { 28 | font-size: $size-2; 29 | color: $grey-darkest; 30 | } 31 | } 32 | 33 | @media(max-width: $app-mobile-break) { 34 | &__message { 35 | height: $mobile-full-screen-pane; 36 | 37 | h1 { 38 | font-size: $size-1 * 2; 39 | } 40 | 41 | &__icon { 42 | font-size: $size-2; 43 | } 44 | 45 | h2 { 46 | font-size: $size-4; 47 | } 48 | } 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/KeyboardShortcutReference/KeyboardShortcutReference.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../variables.scss'; 2 | 3 | .active-task-details__keyboard-reference { 4 | margin-top: 5px; 5 | 6 | .columns .column { 7 | padding-top: 0; 8 | padding-bottom: 0; 9 | } 10 | 11 | .button.active-task-details__keyboard-reference-control { 12 | .control-icon svg { 13 | fill: $grey; 14 | } 15 | 16 | &.is-clear { 17 | font-size: $size-8; 18 | 19 | .control-icon svg { 20 | height: 14px; 21 | vertical-align: text-bottom; 22 | margin-right: 5px; 23 | } 24 | } 25 | } 26 | 27 | .popout-content { 28 | width: 250px; 29 | 30 | dl { 31 | dt { 32 | clear: left; 33 | float: left; 34 | min-width: 40px; 35 | margin-right: 15px; 36 | color: $primary; 37 | font-family: monospace; 38 | font-weight: $weight-bold; 39 | } 40 | 41 | dd { 42 | float: left; 43 | font-size: $size-7; 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/components/Navbar/Navbar.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Navbar from './Navbar' 3 | 4 | let basicProps = null 5 | 6 | beforeEach(() => { 7 | basicProps = { 8 | location: { 9 | pathname: '/some/path' 10 | }, 11 | user: { 12 | id: 357, 13 | isLoggedIn: true, 14 | osmProfile: { 15 | avatarURL: "http://example.com/profilepic.jpg", 16 | }, 17 | }, 18 | } 19 | }) 20 | 21 | test("does not show an admin link if user is not signed in", () => { 22 | basicProps.user.isLoggedIn = false 23 | 24 | const wrapper = shallow( 25 | 26 | ) 27 | 28 | expect(wrapper.find('Link').exists()).toBe(true) 29 | expect(wrapper.find('Link[to="admin"]').exists()).toBe(false) 30 | 31 | expect(wrapper).toMatchSnapshot() 32 | }) 33 | 34 | test("includes a link to user profile page if user is logged in", () => { 35 | const wrapper = shallow( 36 | 37 | ) 38 | 39 | expect(wrapper.find('Link[to="/user/profile"]').exists()).toBe(true) 40 | 41 | expect(wrapper).toMatchSnapshot() 42 | }) 43 | -------------------------------------------------------------------------------- /src/services/Activity/ActivityActionTypes/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ActivityActionType. 5 | */ 6 | export default defineMessages({ 7 | updated: { 8 | id: "Activity.action.updated", 9 | defaultMessage: "Updated" 10 | }, 11 | created: { 12 | id: "Activity.action.created", 13 | defaultMessage: "Created" 14 | }, 15 | deleted: { 16 | id: "Activity.action.deleted", 17 | defaultMessage: "Deleted" 18 | }, 19 | taskViewed: { 20 | id: "Activity.action.taskViewed", 21 | defaultMessage: "Viewed" 22 | }, 23 | taskStatusSet: { 24 | id: "Activity.action.taskStatusSet", 25 | defaultMessage: "Set Status on" 26 | }, 27 | tagAdded: { 28 | id: "Activity.action.tagAdded", 29 | defaultMessage: "Added Tag to" 30 | }, 31 | tagRemoved: { 32 | id: "Activity.action.tagRemoved", 33 | defaultMessage: "Removed Tag from" 34 | }, 35 | questionAnswered: { 36 | id: "Activity.action.questionAnswered", 37 | defaultMessage: "Answered Question on" 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskCommentInput/TaskCommentInput.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import TaskCommentInput from './TaskCommentInput' 3 | 4 | let basicProps = null 5 | 6 | beforeEach(() => { 7 | basicProps = { 8 | value: "foo", 9 | commentChanged: jest.fn(), 10 | intl: {formatMessage: jest.fn()}, 11 | } 12 | }) 13 | 14 | test("it renders a comment input with the given value", () => { 15 | const wrapper = shallow( 16 | 17 | ) 18 | 19 | expect(wrapper.find('input').exists()).toBe(true) 20 | expect(wrapper.find('input').getElement().props.value).toBe(basicProps.value) 21 | 22 | expect(wrapper).toMatchSnapshot() 23 | }) 24 | 25 | test("it signals when the user has modified the input", () => { 26 | const wrapper = shallow( 27 | 28 | ) 29 | 30 | wrapper.find('input').simulate('change', {target: {value: 'bar' }}) 31 | 32 | expect(basicProps.commentChanged.mock.calls.length).toBe(1) 33 | expect(basicProps.commentChanged.mock.calls[0][0]).toBe('bar') 34 | }) 35 | -------------------------------------------------------------------------------- /src/components/TaskPane/ChallengeNameLink/ChallengeNameLink.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import _get from 'lodash/get' 4 | import { Link } from 'react-router-dom' 5 | import ShareLink from '../../ShareLink/ShareLink' 6 | 7 | /** 8 | * ChallengeNameLink displays a linked name of the parent challenge of the 9 | * given task, along with a share link. 10 | * 11 | * @author [Neil Rotstan](https://github.com/nrotstan) 12 | */ 13 | export default class ChallengeNameLink extends Component { 14 | render() { 15 | const challengeBrowseRoute = 16 | `/browse/challenges/${_get(this.props.task, 'parent.id', '')}` 17 | 18 | return ( 19 | 20 | 21 | {_get(this.props.task, 'parent.name')} 22 | 23 | 25 | 26 | ) 27 | } 28 | } 29 | 30 | ChallengeNameLink.propTypes = { 31 | task: PropTypes.object, 32 | } 33 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ChallengeTasksBlock/ChallengeTasksBlock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { DashboardDataTarget } from '../../../../../services/Dashboard/Dashboard' 4 | import { registerBlockType } from '../BlockTypes' 5 | import ViewChallengeTasks from '../../ViewChallengeTasks/ViewChallengeTasks' 6 | import QuickBlock from '../QuickBlock' 7 | import messages from './Messages' 8 | 9 | const descriptor = { 10 | blockKey: 'ChallengeTasksBlock', 11 | label: messages.label, 12 | targets: [DashboardDataTarget.challenge], 13 | minWidth: 4, 14 | defaultWidth: 8, 15 | defaultHeight: 49, 16 | } 17 | 18 | export class ChallengeTasksBlock extends Component { 19 | render() { 20 | return ( 21 | }> 24 | 25 | 26 | ) 27 | } 28 | } 29 | 30 | registerBlockType(ChallengeTasksBlock, descriptor) 31 | -------------------------------------------------------------------------------- /src/components/ActivityTimeline/ActivityTimeline.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | 3 | .timeline.activity-timeline { 4 | .timeline-header { 5 | span { 6 | color: $white; 7 | background-color: $green; 8 | } 9 | } 10 | 11 | .timeline-item { 12 | .timeline-marker { 13 | background-color: $green; 14 | border-color: $green; 15 | } 16 | 17 | .timeline-content { 18 | .heading { 19 | color: $coral; 20 | margin-top: 2px; 21 | } 22 | 23 | .badge { 24 | padding-left: 2rem; 25 | 26 | &::after { 27 | top: 0; 28 | left: 0; 29 | color: $grey-dark; 30 | background-color: $grey-lightest; 31 | box-shadow: none; 32 | } 33 | 34 | &.inverted { 35 | &::after { 36 | background-color: $white; 37 | } 38 | } 39 | } 40 | 41 | .challenge-activity { 42 | margin-top: 20px; 43 | 44 | .challenge-name { 45 | font-weight: $weight-light; 46 | margin-bottom: 5px; 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/MarkdownContent/MarkdownContent.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import remark from 'remark' 3 | import externalLinks from 'remark-external-links' 4 | import reactRenderer from 'remark-react' 5 | import PropTypes from 'prop-types' 6 | 7 | /** 8 | * MarkdownContent normalizes and renders the content of the given markdown 9 | * string as formatted Markdown. 10 | * 11 | * @author [Neil Rotstan](https://github.com/nrotstan) 12 | */ 13 | export default class MarkdownContent extends Component { 14 | render() { 15 | if (!this.props.markdown) { 16 | return null 17 | } 18 | 19 | // Replace any occurrences of \r\n with newlines. 20 | const normalizedMarkdown = this.props.markdown.replace(/\r\n/mg, "\n\n") 21 | 22 | return ( 23 |
24 | { 25 | remark().use(externalLinks, {target: '_blank', rel: ['nofollow']}) 26 | .use(reactRenderer).processSync(normalizedMarkdown).contents 27 | } 28 |
29 | ) 30 | } 31 | } 32 | 33 | MarkdownContent.propTypes = { 34 | markdown: PropTypes.string, 35 | } 36 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeKeywords/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeKeywords. 5 | */ 6 | export default defineMessages({ 7 | navigation: { 8 | id: "Challenge.keywords.navigation", 9 | defaultMessage: "Roads / Pedestrian / Cycleways", 10 | }, 11 | water: { 12 | id: "Challenge.keywords.water", 13 | defaultMessage: "Water", 14 | }, 15 | pointsOfInterest: { 16 | id: "Challenge.keywords.pointsOfInterest", 17 | defaultMessage: "Points / Areas of Interest", 18 | }, 19 | buildings: { 20 | id: "Challenge.keywords.buildings", 21 | defaultMessage: "Buildings", 22 | }, 23 | landUse: { 24 | id: "Challenge.keywords.landUse", 25 | defaultMessage: "Land Use / Administrative Boundaries", 26 | }, 27 | transit: { 28 | id: "Challenge.keywords.transit", 29 | defaultMessage: "Transit", 30 | }, 31 | other: { 32 | id: "Challenge.keywords.other", 33 | defaultMessage: "Other", 34 | }, 35 | any: { 36 | id: "Challenge.keywords.any", 37 | defaultMessage: "Anything", 38 | }, 39 | }) 40 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectsDashboard/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectsDashboard 5 | */ 6 | export default defineMessages({ 7 | newProject: { 8 | id: 'Admin.ProjectsDashboard.newProject', 9 | defaultMessage: "Add Project", 10 | }, 11 | 12 | help: { 13 | id: "Admin.ProjectsDashboard.help.info", 14 | defaultMessage: "Projects serve as a means of grouping related " + 15 | "challenges together. All challenges must belong " + 16 | "to a project.", 17 | }, 18 | 19 | placeholder: { 20 | id: "Admin.ProjectsDashboard.search.placeholder", 21 | defaultMessage: "Project or Challenge Name", 22 | }, 23 | 24 | addChallengeTooltip: { 25 | id: "Admin.Project.controls.addChallenge.tooltip", 26 | defaultMessage: "New Challenge", 27 | }, 28 | 29 | regenerateHomeProject: { 30 | id: "Admin.ProjectsDashboard.regenerateHomeProject", 31 | defaultMessage: "Please sign out and sign back in to regenerate a " + 32 | "fresh home project.", 33 | }, 34 | }) 35 | -------------------------------------------------------------------------------- /src/components/ScreenTooNarrow/ScreenTooNarrow.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import Sprites from '../Sprites/Sprites' 4 | import SvgSymbol from '../SvgSymbol/SvgSymbol' 5 | import messages from './Messages' 6 | import './ScreenTooNarrow.css' 7 | 8 | /** 9 | * ScreenTooNarrow displays a message indicating that the user's screen/window 10 | * is too narrow to display the current page 11 | * 12 | * @author [Neil Rotstan](https://github.com/nrotstan) 13 | */ 14 | export default class ScreenTooNarrow extends Component { 15 | render() { 16 | return ( 17 |
18 |
19 | 20 |
21 |

22 |

23 | 24 |

25 |
26 |
27 | 28 |
29 | ) 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2018 Martijn van Exel. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 9 | of the Software, and to permit persons to whom the Software is furnished to do 10 | so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/components/AdminPane/MetricsOverview/MetricsOverview.scss: -------------------------------------------------------------------------------- 1 | @import '../../../variables.scss'; 2 | 3 | .admin { 4 | .metrics-overview { 5 | margin-bottom: 1.5rem; 6 | 7 | .stat { 8 | margin: 15px; 9 | 10 | .name { 11 | font-size: $size-7; 12 | color: $grey; 13 | text-align: center; 14 | } 15 | 16 | .value { 17 | font-size: $size-4; 18 | color: $green; 19 | text-align: center; 20 | } 21 | 22 | .secondary-value { 23 | font-size: $size-6; 24 | color: $green; 25 | text-align: center; 26 | } 27 | } 28 | 29 | .task-stats { 30 | display: flex; 31 | justify-content: space-between; 32 | margin-top: 40px; 33 | 34 | .completion-stats { 35 | min-width: 250px; 36 | width: 300px; 37 | 38 | h5 { 39 | color: $primary; 40 | text-align: center; 41 | } 42 | 43 | .completion-progress { 44 | margin-top: 15px; 45 | 46 | &.evaluated-by-user { 47 | margin-top: 30px; 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/services/Task/TaskPriority/TaskPriority.js: -------------------------------------------------------------------------------- 1 | import _map from 'lodash/map' 2 | import _invert from 'lodash/invert' 3 | import _fromPairs from 'lodash/fromPairs' 4 | import messages from './Messages' 5 | 6 | /** 7 | * Constants defining task priority levels. These statuses are defined on the 8 | * server. 9 | */ 10 | export const TASK_PRIORITY_HIGH = 0 11 | export const TASK_PRIORITY_MEDIUM = 1 12 | export const TASK_PRIORITY_LOW = 2 13 | 14 | export const TaskPriority = Object.freeze({ 15 | high: TASK_PRIORITY_HIGH, 16 | medium: TASK_PRIORITY_MEDIUM, 17 | low: TASK_PRIORITY_LOW, 18 | }) 19 | 20 | export const keysByPriority = Object.freeze(_invert(TaskPriority)) 21 | 22 | /** 23 | * Returns an object mapping priority values to raw internationalized 24 | * messages suitable for use with FormattedMessage or formatMessage. 25 | */ 26 | export const messagesByPriority = _fromPairs( 27 | _map(messages, (message, key) => [TaskPriority[key], message]) 28 | ) 29 | 30 | /** Returns object containing localized labels */ 31 | export const taskPriorityLabels = intl => _fromPairs( 32 | _map(messages, (message, key) => [key, intl.formatMessage(message)]) 33 | ) 34 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/StatusRadarBlock/StatusRadarBlock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { DashboardDataTarget } from '../../../../../services/Dashboard/Dashboard' 4 | import { registerBlockType } from '../BlockTypes' 5 | import CompletionRadar from '../../CompletionRadar/CompletionRadar' 6 | import QuickBlock from '../QuickBlock' 7 | import messages from './Messages' 8 | import './StatusRadarBlock.css' 9 | 10 | const descriptor = { 11 | blockKey: 'StatusRadarBlock', 12 | label: messages.label, 13 | targets: [DashboardDataTarget.challenges, DashboardDataTarget.challenge], 14 | minWidth: 3, 15 | defaultWidth: 4, 16 | defaultHeight: 12, 17 | } 18 | 19 | export class StatusRadarBlock extends Component { 20 | render() { 21 | return ( 22 | }> 25 | 26 | 27 | ) 28 | } 29 | } 30 | 31 | registerBlockType(StatusRadarBlock, descriptor) 32 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/CompletionProgressBlock/CompletionProgressBlock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { DashboardDataTarget } from '../../../../../services/Dashboard/Dashboard' 4 | import { registerBlockType } from '../BlockTypes' 5 | import ChallengeProgress from '../../../../ChallengeProgress/ChallengeProgress' 6 | import QuickBlock from '../QuickBlock' 7 | import messages from './Messages' 8 | 9 | const descriptor = { 10 | blockKey: 'CompletionProgressBlock', 11 | label: messages.label, 12 | targets: [DashboardDataTarget.challenges, DashboardDataTarget.challenge], 13 | minWidth: 3, 14 | defaultWidth: 4, 15 | defaultHeight: 5, 16 | } 17 | 18 | export class CompletionProgressBlock extends Component { 19 | render() { 20 | return ( 21 | }> 24 | 25 | 26 | ) 27 | } 28 | } 29 | 30 | registerBlockType(CompletionProgressBlock, descriptor) 31 | -------------------------------------------------------------------------------- /src/services/Leaderboard/Leaderboard.js: -------------------------------------------------------------------------------- 1 | import { defaultRoutes as api } from '../Server/Server' 2 | import _isArray from 'lodash/isArray' 3 | import Endpoint from '../Server/Endpoint' 4 | import startOfDay from 'date-fns/start_of_day' 5 | 6 | /** 7 | * Retrieve leaderboard data from the server for the given date range and 8 | * filters, returning a Promise that resolves to the leaderboard data. Note 9 | * that leaderboard data is *not* stored in the redux store. 10 | */ 11 | export const fetchLeaderboard = function(startDate=null, endDate=null, onlyEnabled=true, 12 | forProjects=null, forChallenges=null, limit=10) { 13 | const params = { 14 | limit, 15 | } 16 | 17 | if (startDate) { 18 | params.start = startOfDay(startDate).toISOString() 19 | } 20 | 21 | if (endDate) { 22 | params.end = startOfDay(endDate).toISOString() 23 | } 24 | 25 | if (_isArray(forProjects)) { 26 | params.projectIds = forProjects.join(',') 27 | } 28 | 29 | if (_isArray(forChallenges)) { 30 | params.challengeIds = forChallenges.join(',') 31 | } 32 | 33 | return new Endpoint(api.users.leaderboard, {params}).execute() 34 | } 35 | -------------------------------------------------------------------------------- /src/components/HomePane/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with HomePane. 5 | */ 6 | export default defineMessages({ 7 | header: { 8 | id: 'HomePane.header', 9 | defaultMessage: "Be an instant contributor to the world's maps", 10 | }, 11 | 12 | feedbackHeader: { 13 | id: 'HomePane.feedback.header', 14 | defaultMessage: "Feedback", 15 | }, 16 | 17 | filterTagIntro: { 18 | id: 'HomePane.filterTagIntro', 19 | defaultMessage: 'Find tasks that address efforts important to you.', 20 | }, 21 | 22 | filterLocationIntro: { 23 | id: 'HomePane.filterLocationIntro', 24 | defaultMessage: 'Make fixes in local areas you care about.', 25 | }, 26 | 27 | filterDifficultyIntro: { 28 | id: 'HomePane.filterDifficultyIntro', 29 | defaultMessage: 'Work at your own level, from novice to expert.', 30 | }, 31 | 32 | createChallenges: { 33 | id: 'HomePane.createChallenges', 34 | defaultMessage: 'Create tasks for others to help improve map data.', 35 | }, 36 | 37 | subheader: { 38 | id: 'HomePane.subheader', 39 | defaultMessage:"Get Started", 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ActiveTaskDetails. 5 | */ 6 | export default defineMessages({ 7 | info: { 8 | id: "ActiveTask.controls.info.tooltip", 9 | defaultMessage: "Task Details", 10 | }, 11 | 12 | viewComments: { 13 | id: "ActiveTask.controls.comments.tooltip", 14 | defaultMessage: "View Comments", 15 | }, 16 | 17 | comments: { 18 | id: 'ActiveTask.subheading.comments', 19 | defaultMessage: 'Comments', 20 | }, 21 | 22 | challengeHeading: { 23 | id: 'ActiveTask.heading', 24 | defaultMessage: 'Challenge Information', 25 | }, 26 | 27 | instructions: { 28 | id: 'ActiveTask.subheading.instructions', 29 | defaultMessage: 'Instructions', 30 | }, 31 | 32 | location: { 33 | id: 'ActiveTask.subheading.location', 34 | defaultMessage: 'Location', 35 | }, 36 | 37 | progress: { 38 | id: 'ActiveTask.subheading.progress', 39 | defaultMessage: 'Challenge Progress', 40 | }, 41 | 42 | social: { 43 | id: 'ActiveTask.subheading.social', 44 | defaultMessage: 'Share', 45 | }, 46 | }) 47 | -------------------------------------------------------------------------------- /src/components/Bulma/Modal.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | 5 | /** 6 | * Modal displays a Bulma modal dialogue if the `isActive` prop is true. 7 | * Content of the dialog should be passed as children. It includes a background 8 | * that will invoke `onClose` if the user clicks off the modal. If a close 9 | * button is desired, it should be provided as part of the the modal content. 10 | * 11 | * @author [Neil Rotstan](https://github.com/nrotstan) 12 | */ 13 | export default class Modal extends Component { 14 | render() { 15 | return ( 16 |
18 |
19 |
{this.props.children}
20 |
21 | ) 22 | } 23 | } 24 | 25 | Modal.propTypes = { 26 | /** Determines if the modal is displayed or not */ 27 | isActive: PropTypes.bool.isRequired, 28 | /** Invoked to close the modal */ 29 | onClose: PropTypes.func.isRequired, 30 | } 31 | -------------------------------------------------------------------------------- /src/components/Ribbon/Ribbon.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | $ribbon-color: lighten($primary, 10%); 4 | 5 | .ribbon { 6 | width: 150px; 7 | height: 122px; 8 | overflow: hidden; 9 | position: absolute; 10 | } 11 | .ribbon::before, 12 | .ribbon::after { 13 | position: absolute; 14 | content: ''; 15 | display: block; 16 | border: 6px solid darken($ribbon-color, 10%); 17 | } 18 | .ribbon span { 19 | position: absolute; 20 | display: block; 21 | width: 175px; 22 | padding: 2px 0; 23 | font-size: $size-5; 24 | background-color: $ribbon-color; 25 | box-shadow: 0px 2px 4px $box-shadow-color; 26 | color: $white; 27 | text-align: center; 28 | z-index: 1; 29 | } 30 | 31 | /* top left*/ 32 | .ribbon-top-left { 33 | top: -6px; 34 | left: -8px; 35 | } 36 | .ribbon-top-left::before, 37 | .ribbon-top-left::after { 38 | border-top-color: transparent; 39 | border-left-color: transparent; 40 | } 41 | .ribbon-top-left::before { 42 | top: -6px; 43 | right: 30px; 44 | } 45 | .ribbon-top-left::after { 46 | bottom: 49px; 47 | left: -1px; 48 | border-width: 4px; 49 | } 50 | .ribbon-top-left span { 51 | right: 24px; 52 | top: 12px; 53 | transform: rotate(-31deg); 54 | } 55 | -------------------------------------------------------------------------------- /src/services/Dashboard/ChallengeFilter/ChallengeFilter.js: -------------------------------------------------------------------------------- 1 | import _fromPairs from 'lodash/fromPairs' 2 | import _map from 'lodash/map' 3 | import messages from './Messages' 4 | 5 | export const CHALLENGE_FILTER_VISIBLE = 'visible' 6 | export const CHALLENGE_FILTER_PINNED = 'pinned' 7 | 8 | export const ChallengeFilter = { 9 | visible: CHALLENGE_FILTER_VISIBLE, 10 | pinned: CHALLENGE_FILTER_PINNED, 11 | } 12 | 13 | export const defaultChallengeFilters = function() { 14 | return { 15 | [CHALLENGE_FILTER_VISIBLE]: false, 16 | [CHALLENGE_FILTER_PINNED]: false, 17 | } 18 | } 19 | 20 | export const challengePassesFilters = function(challenge, manager, pins, challengeFilters) { 21 | if (challengeFilters.visible && !challenge.enabled) { 22 | return false 23 | } 24 | 25 | if (challengeFilters.pinned && pins.indexOf(challenge.id) === -1) { 26 | return false 27 | } 28 | 29 | return true 30 | } 31 | 32 | /** 33 | * Returns an object mapping challenge filters to raw internationalized 34 | * messages suitable for use with FormattedMessage or formatMessage. 35 | */ 36 | export const messagesByFilter = _fromPairs( 37 | _map(messages, (message, key) => [ChallengeFilter[key], message]) 38 | ) 39 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ProjectAboutBlock/ProjectAboutBlock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FormattedMessage, injectIntl } from 'react-intl' 3 | import { DashboardDataTarget } from '../../../../../services/Dashboard/Dashboard' 4 | import { registerBlockType } from '../BlockTypes' 5 | import MarkdownContent from '../../../../MarkdownContent/MarkdownContent' 6 | import QuickBlock from '../QuickBlock' 7 | import messages from './Messages' 8 | import './ProjectAboutBlock.css' 9 | 10 | const descriptor = { 11 | blockKey: 'ProjectAboutBlock', 12 | label: messages.label, 13 | targets: [DashboardDataTarget.projects, DashboardDataTarget.project], 14 | minWidth: 3, 15 | defaultWidth: 6, 16 | defaultHeight: 7, 17 | } 18 | 19 | export class ProjectAboutBlock extends Component { 20 | render() { 21 | return ( 22 | }> 25 | 26 | 27 | ) 28 | } 29 | } 30 | 31 | registerBlockType(injectIntl(ProjectAboutBlock), descriptor) 32 | -------------------------------------------------------------------------------- /src/components/Leaderboard/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with Leaderboard 5 | */ 6 | export default defineMessages({ 7 | leaderboardTitle: { 8 | id: "Leaderboard.title", 9 | defaultMessage: "Leaderboard for", 10 | }, 11 | 12 | scoringMethodLabel: { 13 | id: "Leaderboard.scoringMethod.label", 14 | defaultMessage: "Scoring method", 15 | }, 16 | 17 | scoringExplanation: { 18 | id: "Leaderboard.scoringMethod.explanation", 19 | defaultMessage: ` 20 | ##### Points are awarded per completed task as follows: 21 | 22 | | Status | Points | 23 | | :------------ | -----: | 24 | | Fixed | 5 | 25 | | Not an Issue | 3 | 26 | | Already Fixed | 3 | 27 | | Too Hard | 1 | 28 | | Skipped | 0 | 29 | ` 30 | }, 31 | 32 | userPoints: { 33 | id: "Leaderboard.user.points", 34 | defaultMessage: "Points", 35 | }, 36 | 37 | userTopChallenges: { 38 | id: "Leaderboard.user.topChallenges", 39 | defaultMessage: "Top Challenges", 40 | }, 41 | 42 | noLeaders: { 43 | id: "Leaderboard.users.none", 44 | defaultMessage: "No users for time period", 45 | }, 46 | }) 47 | 48 | -------------------------------------------------------------------------------- /src/components/ShareLink/ShareLink.scss: -------------------------------------------------------------------------------- 1 | @import 'mixins.scss'; 2 | 3 | .share-link.dropdown { 4 | position: static; // allow content to float above sidebar 5 | 6 | .dropdown-trigger { 7 | svg { 8 | fill: $grey-lighter; 9 | height: 15px; 10 | width: auto; 11 | } 12 | 13 | &:hover { 14 | cursor: pointer; 15 | } 16 | } 17 | 18 | .menu-wrapper { 19 | position: absolute; // positions dropdown menu relative to trigger 20 | 21 | .dropdown-menu { 22 | top: 0px; 23 | left: 25px; 24 | 25 | .dropdown-content { 26 | font-size: $size-7; 27 | color: $grey; 28 | padding: 5px 10px 5px 15px; 29 | display: flex; 30 | justify-content: flex-start; 31 | align-items: center; 32 | 33 | .share-link__text { 34 | word-break: keep-all; 35 | } 36 | 37 | .share-link__copy-button { 38 | @include invert-on-hover($green, $white) 39 | margin-left: 10px; 40 | font-size: $size-7; 41 | padding: 0px 10px 0px 5px; 42 | 43 | svg { 44 | width: 15px; 45 | margin-bottom: 1px; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskCommentInput/TaskCommentInput.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import Messages from './Messages' 5 | import './TaskCommentInput.css' 6 | 7 | /** 8 | * TaskCommentInput renders an unmanaged input for allowing users to add an 9 | * optional comment to a task when completing it. 10 | * 11 | * @author [Neil Rotstan](https://github.com/nrotstan) 12 | */ 13 | export default class TaskCommentInput extends Component { 14 | handleChange = (e) => { 15 | this.props.commentChanged(e.target.value) 16 | } 17 | 18 | render() { 19 | return ( 20 |
21 | 24 |
25 | ) 26 | } 27 | } 28 | 29 | TaskCommentInput.propTypes = { 30 | value: PropTypes.string, 31 | commentChanged: PropTypes.func.isRequired, 32 | } 33 | 34 | TaskCommentInput.defaultProps = { 35 | value: "", 36 | } 37 | -------------------------------------------------------------------------------- /src/components/TaskPane/ChallengeShareControls/ChallengeShareControls.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ChallengeShareControls from './ChallengeShareControls' 3 | import _cloneDeep from 'lodash/cloneDeep' 4 | 5 | const propsFixture = { 6 | challenge: { 7 | id: 123, 8 | }, 9 | } 10 | 11 | let basicProps = null 12 | 13 | beforeEach(() => { 14 | basicProps = _cloneDeep(propsFixture) 15 | }) 16 | 17 | test("renders with props as expected", () => { 18 | const wrapper = shallow( 19 | 20 | ) 21 | 22 | expect(wrapper.find('.challenge-share-controls').exists()).toBe(true) 23 | expect(wrapper.find('.share-icon').length).toBe(3) 24 | expect(wrapper).toMatchSnapshot() 25 | }) 26 | 27 | test("does not explode with null challenge", () => { 28 | const wrapper = shallow( 29 | 30 | ) 31 | 32 | expect(wrapper).toMatchSnapshot() 33 | }) 34 | 35 | test("renders with props className in encapsulating div", () => { 36 | const wrapper = shallow( 37 | 38 | ) 39 | 40 | expect(wrapper.find('.my-test').exists()).toBe(true) 41 | expect(wrapper).toMatchSnapshot() 42 | }) 43 | -------------------------------------------------------------------------------- /src/components/UserProfile/PersonalInfo.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import { FormattedDate } from 'react-intl' 5 | 6 | export default class PersonalInfo extends Component { 7 | render() { 8 | return ( 9 |
10 |
11 | Avatar 12 |
13 | 14 |
15 |
16 | {this.props.user.osmProfile.displayName} 17 |
18 |
19 | User since: 20 | 22 | 23 |
24 |
25 |
26 | ) 27 | } 28 | } 29 | 30 | PersonalInfo.propTypes = { 31 | user: PropTypes.object, 32 | } 33 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ChallengeOverviewBlock/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ChallengeOverviewBlock 5 | */ 6 | export default defineMessages({ 7 | label: { 8 | id: "GridBlocks.ChallengeOverviewBlock.label", 9 | defaultMessage: "Challenge Overview", 10 | }, 11 | 12 | title: { 13 | id: "GridBlocks.ChallengeOverviewBlock.title", 14 | defaultMessage: "Overview", 15 | }, 16 | 17 | creationDate: { 18 | id: "GridBlocks.ChallengeOverviewBlock.fields.creationDate.label", 19 | defaultMessage: "Created:", 20 | }, 21 | 22 | lastModifiedDate: { 23 | id: "GridBlocks.ChallengeOverviewBlock.fields.lastModifiedDate.label", 24 | defaultMessage: "Modified:", 25 | }, 26 | 27 | tasksRefreshDate: { 28 | id: "GridBlocks.ChallengeOverviewBlock.fields.tasksRefreshDate.label", 29 | defaultMessage: "Tasks Refreshed:", 30 | }, 31 | 32 | status: { 33 | id: "GridBlocks.ChallengeOverviewBlock.fields.status.label", 34 | defaultMessage: "Status:", 35 | }, 36 | 37 | visibleLabel: { 38 | id: "GridBlocks.ChallengeOverviewBlock.fields.enabled.label", 39 | defaultMessage: "Visible:", 40 | }, 41 | }) 42 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectManagers/ProjectManagers.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .project-managers { 4 | 5 | &__manager { 6 | display: flex; 7 | justify-content: space-between; 8 | align-items: center; 9 | font-size: $size-7; 10 | margin-top: 5px; 11 | 12 | &__about { 13 | display: flex; 14 | align-items: center; 15 | } 16 | 17 | &__profile-pic { 18 | img { 19 | border-radius: 50%; 20 | } 21 | } 22 | 23 | &__name { 24 | margin-left: 5px; 25 | } 26 | 27 | &__controls { 28 | display: flex; 29 | justify-content: space-between; 30 | align-items: center; 31 | min-width: 100px; 32 | } 33 | 34 | &__role, &__role-placeholder { 35 | margin: 0 1em; 36 | } 37 | 38 | &__control-placeholder { 39 | width: 15px; 40 | } 41 | } 42 | 43 | &__add-manager { 44 | margin-top: 30px; 45 | 46 | h3 { 47 | margin-bottom: 10px; 48 | font-size: $size-5; 49 | } 50 | 51 | &__form { 52 | display: flex; 53 | justify-content: space-between; 54 | align-items: center; 55 | 56 | select { 57 | margin-left: 1em; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/RebuildTasksControl/RebuildTasksControl.scss: -------------------------------------------------------------------------------- 1 | @import "mixins.scss"; 2 | 3 | .rebuild-tasks-control { 4 | .modal .modal-content .message { 5 | background-color: $white; 6 | } 7 | 8 | &__explanation { 9 | @include markdown-content(); 10 | background-color: $grey-lightest-more; 11 | border-radius: $radius-medium; 12 | padding: 1em; 13 | 14 | li { 15 | margin-bottom: 0.5em; 16 | } 17 | } 18 | 19 | &__options { 20 | background-color: $white; 21 | margin: 20px 0; 22 | } 23 | 24 | &__upload-geojson { 25 | margin-top: 1em; 26 | 27 | form.rjsf .dropzone { 28 | background-color: transparent; 29 | } 30 | } 31 | 32 | &__remove-unmatched-option { 33 | label.checkbox { 34 | color: $primary 35 | } 36 | 37 | input { 38 | margin-right: 0.5em; 39 | } 40 | } 41 | 42 | &__warning { 43 | color: $danger; 44 | font-weight: $weight-semibold; 45 | } 46 | 47 | &__modal-controls { 48 | margin-top: 20px; 49 | display: flex; 50 | justify-content: flex-end; 51 | 52 | button.button { 53 | margin-right: 15px; 54 | 55 | &:last-child { 56 | margin-right: 0; 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/components/CommentList/CommentCountBadge/CommentCountBadge.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import classNames from 'classnames' 4 | import SvgSymbol from '../../SvgSymbol/SvgSymbol' 5 | import './CommentCountBadge.css' 6 | 7 | /** 8 | * CommentCountBadge renders an icon with a Bulma badge containing a count of 9 | * the comments in the given comment list. 10 | * 11 | * @see See https://wikiki.github.io/elements/badge/ 12 | * 13 | * @author [Neil Rotstan](https://github.com/nrotstan) 14 | */ 15 | export default class CommentCountBadge extends Component { 16 | render() { 17 | return ( 18 |
19 | 22 | 23 | 24 |
25 | ) 26 | } 27 | } 28 | 29 | CommentCountBadge.propTypes = { 30 | comments: PropTypes.array, 31 | tooltip: PropTypes.string, 32 | } 33 | 34 | CommentCountBadge.defaultProps = { 35 | comments: [], 36 | } 37 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectManagers/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectManagers 5 | */ 6 | export default defineMessages({ 7 | noManagers: { 8 | id: "Admin.ProjectManagers.noManagers", 9 | defaultMessage: "No Managers" 10 | }, 11 | 12 | addManager: { 13 | id: "Admin.ProjectManagers.addManager", 14 | defaultMessage: "Add Project Manager" 15 | }, 16 | 17 | projectOwner: { 18 | id: "Admin.ProjectManagers.projectOwner", 19 | defaultMessage: "Owner" 20 | }, 21 | 22 | removeManagerTooltip: { 23 | id: "Admin.ProjectManagers.controls.removeManager.tooltip", 24 | defaultMessage: "Remove Manager" 25 | }, 26 | 27 | removeManagerConfirmation: { 28 | id: "Admin.ProjectManagers.controls.removeManager.confirmation", 29 | defaultMessage: "Are you sure you wish to remove this manager from the project?" 30 | }, 31 | 32 | chooseRole: { 33 | id: "Admin.ProjectManagers.controls.selectRole.choose.label", 34 | defaultMessage: "Choose Role" 35 | }, 36 | 37 | osmUsername: { 38 | id: "Admin.ProjectManagers.controls.chooseOSMUser.placeholder", 39 | defaultMessage: "OpenStreetMap username" 40 | }, 41 | }) 42 | 43 | -------------------------------------------------------------------------------- /src/components/UserProfile/UserProfile.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { UserProfile } from './UserProfile' 3 | 4 | let basicProps = null 5 | let createdDate = Date.UTC(2017, 1, 23) 6 | 7 | beforeEach(() => { 8 | basicProps = { 9 | user: { 10 | id: 357, 11 | isLoggedIn: true, 12 | created: createdDate, 13 | osmProfile: { 14 | avatarURL: "some/url" 15 | } 16 | }, 17 | intl: {formatMessage: jest.fn(m => m.defaultMessage)}, 18 | loadCompleteUser: jest.fn(), 19 | resetAPIKey: () => null, 20 | } 21 | }) 22 | 23 | test('it just shows a sign-in option if the user is not logged in', () => { 24 | basicProps.user.isLoggedIn = false 25 | 26 | const wrapper = shallow( 27 | 28 | ) 29 | 30 | expect(wrapper.find('.user-profile--signin').exists()).toBe(true) 31 | expect(wrapper.find('.user-profile__personal').exists()).toBe(false) 32 | 33 | expect(wrapper).toMatchSnapshot() 34 | }) 35 | 36 | test('it shows user profile data if the user is logged in', () => { 37 | const wrapper = shallow( 38 | 39 | ) 40 | 41 | expect(wrapper.find('PersonalInfo').exists()).toBe(true) 42 | 43 | expect(wrapper).toMatchSnapshot() 44 | }) 45 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/ProjectCountBlock/ProjectCountBlock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import { FormattedMessage } from 'react-intl' 4 | import { DashboardDataTarget } from '../../../../../services/Dashboard/Dashboard' 5 | import { registerBlockType } from '../BlockTypes' 6 | import QuickBlock from '../QuickBlock' 7 | import messages from './Messages' 8 | 9 | const descriptor = { 10 | blockKey: 'ProjectCountBlock', 11 | label: messages.label, 12 | targets: [DashboardDataTarget.projects], 13 | minWidth: 2, 14 | defaultWidth: 6, 15 | defaultHeight: 4, 16 | } 17 | 18 | export class ProjectCountBlock extends Component { 19 | render() { 20 | return ( 21 | }> 24 |

{this.props.filteredProjects.length}

25 |
26 | ) 27 | } 28 | } 29 | 30 | ProjectCountBlock.propTypes = { 31 | blockProjectFilters: PropTypes.object, 32 | updateBlockConfiguration: PropTypes.func.isRequired, 33 | filteredProjects: PropTypes.array, 34 | } 35 | 36 | registerBlockType(ProjectCountBlock, descriptor) 37 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectCard/Messages.js: -------------------------------------------------------------------------------- 1 | import { defineMessages } from 'react-intl' 2 | 3 | /** 4 | * Internationalized messages for use with ProjectCard 5 | */ 6 | export default defineMessages({ 7 | challengesTabLabel: { 8 | id: "Admin.ProjectCard.tabs.challenges.label", 9 | defaultMessage: "Challenges", 10 | }, 11 | 12 | detailsTabLabel: { 13 | id: "Admin.ProjectCard.tabs.details.label", 14 | defaultMessage: "Details", 15 | }, 16 | 17 | managersTabLabel: { 18 | id: "Admin.ProjectCard.tabs.managers.label", 19 | defaultMessage: "Managers", 20 | }, 21 | 22 | enabledTooltip: { 23 | id: "Admin.Project.fields.enabled.tooltip", 24 | defaultMessage: "Enabled", 25 | }, 26 | 27 | disabledTooltip: { 28 | id: "Admin.Project.fields.disabled.tooltip", 29 | defaultMessage: "Disabled", 30 | }, 31 | 32 | editProjectTooltip: { 33 | id: "Admin.ProjectCard.controls.editProject.tooltip", 34 | defaultMessage: "Edit Project", 35 | }, 36 | 37 | editProjectLabel: { 38 | id: "Admin.ProjectCard.controls.editProject.label", 39 | defaultMessage: "Edit", 40 | }, 41 | 42 | challengePreviewHeader: { 43 | id: "Admin.Project.headers.challengePreview", 44 | defaultMessage: "Challenge Matches", 45 | } 46 | }) 47 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ProjectList/ProjectList.scss: -------------------------------------------------------------------------------- 1 | @import 'variables.scss'; 2 | 3 | .admin__manage__managed-item-list.project-list { 4 | font-size: $size-5; 5 | margin: 0px 20px 0 0; 6 | display: flex; 7 | flex-wrap: wrap; 8 | 9 | &.compact-view { 10 | flex-direction: column; 11 | flex-wrap: no-wrap; 12 | } 13 | 14 | .project-list__favorites { 15 | display: flex; 16 | flex-wrap: wrap; 17 | } 18 | 19 | .project-list__heading { 20 | color: $green; 21 | margin-top: 40px; 22 | padding-bottom: 20px; 23 | margin-bottom: 20px; 24 | border-bottom: 2px solid $grey-lightest-less; 25 | 26 | h2 { 27 | font-weight: $weight-semibold; 28 | } 29 | } 30 | 31 | .columns { 32 | margin-left: 0; 33 | margin-right: 0; 34 | } 35 | 36 | svg.icon { 37 | height: 15px; 38 | width: auto; 39 | } 40 | 41 | .column.edit-control { 42 | font-size: $size-7; 43 | font-weight: $weight-normal; 44 | } 45 | 46 | .project-list-item { 47 | align-items: center; 48 | 49 | &:not(:last-child) { 50 | border-bottom: none; 51 | } 52 | 53 | &__project-name { 54 | color: $primary 55 | } 56 | } 57 | 58 | .after-results { 59 | width: 100%; 60 | text-align: center; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/Navbar/AccountNavItem/AccountNavItem.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { AccountNavItem } from './AccountNavItem' 3 | 4 | let basicProps = null 5 | 6 | beforeEach(() => { 7 | basicProps = { 8 | logoutUser: jest.fn(), 9 | intl: { 10 | formatMessage: jest.fn(), 11 | }, 12 | user: { 13 | id: 357, 14 | isLoggedIn: true, 15 | osmProfile: { 16 | avatarURL: "some/url" 17 | } 18 | } 19 | } 20 | }) 21 | 22 | test("shows only a sign-in button if the user is not logged in", () => { 23 | basicProps.user.isLoggedIn = false 24 | 25 | const wrapper = shallow( 26 | 27 | ) 28 | 29 | expect(wrapper.find('.navbar__account-nav-item__signin').exists()).toBe(true) 30 | expect(wrapper.find('.navbar__account-nav-item__dropdown').exists()).toBe(false) 31 | 32 | expect(wrapper).toMatchSnapshot() 33 | }) 34 | 35 | test("shows a dropdown menu if user is logged in", () => { 36 | const wrapper = shallow( 37 | 38 | ) 39 | 40 | expect(wrapper.find('.navbar__account-nav-item__signin').exists()).toBe(false) 41 | expect(wrapper.find('.navbar__account-nav-item__dropdown').exists()).toBe(true) 42 | 43 | expect(wrapper).toMatchSnapshot() 44 | }) 45 | -------------------------------------------------------------------------------- /src/components/HOCs/WithProgress/WithProgress.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import _upperFirst from 'lodash/upperFirst' 3 | 4 | /** 5 | * WithProgress manages an inProgress status for a named operation along with 6 | * an optional number of steps completed so far as part of the operation 7 | * 8 | * @author [Neil Rotstan](https://github.com/nrotstan) 9 | */ 10 | const WithProgress = function(WrappedComponent, operationName) { 11 | return class extends Component { 12 | state = { 13 | inProgress: false, 14 | stepsCompleted: 0, 15 | } 16 | 17 | updateProgress = (inProgress, stepsCompleted) => { 18 | this.setState({inProgress, stepsCompleted}) 19 | } 20 | 21 | render() { 22 | // Merge in any other progress statuses 23 | const allProgress = Object.assign({}, this.props.progress, { 24 | [operationName]: { 25 | inProgress: this.state.inProgress, 26 | stepsCompleted: this.state.stepsCompleted, 27 | } 28 | }) 29 | 30 | return 33 | } 34 | } 35 | } 36 | 37 | export default WithProgress 38 | -------------------------------------------------------------------------------- /src/components/ChallengePane/ChallengeFilterSubnav/ChallengeFilterSubnav.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { ChallengeFilterSubnav } from './ChallengeFilterSubnav' 3 | import _cloneDeep from 'lodash/cloneDeep' 4 | 5 | const propsFixture = { 6 | user: { 7 | id: 11, 8 | savedChallenges: [], 9 | }, 10 | challenges: [ 11 | { 12 | id: 309, 13 | name: "Challenge 309", 14 | blurb: "Challenge 309 blurb", 15 | description: "Challenge 309 description", 16 | parent: { 17 | displayName: "foo", 18 | } 19 | }, 20 | { 21 | id: 311, 22 | name: "Challenge 311", 23 | blurb: "Challenge 311 blurb", 24 | description: "Challenge 311 description", 25 | parent: { 26 | displayName: "bar", 27 | } 28 | } 29 | ], 30 | setSearch: jest.fn(), 31 | clearSearch: jest.fn(), 32 | } 33 | 34 | let basicProps = null 35 | 36 | beforeEach(() => { 37 | basicProps = _cloneDeep(propsFixture) 38 | basicProps.intl = {formatMessage: jest.fn()} 39 | }) 40 | 41 | test("renders with props as expected", () => { 42 | const wrapper = shallow( 43 | 44 | ) 45 | 46 | expect(wrapper.find('.challenge-filter-subnav').exists()).toBe(true) 47 | expect(wrapper).toMatchSnapshot() 48 | }) 49 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/BurndownChartBlock/BurndownChartBlock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { DashboardDataTarget } from '../../../../../services/Dashboard/Dashboard' 4 | import { registerBlockType } from '../BlockTypes' 5 | import BurndownChart from '../../BurndownChart/BurndownChart' 6 | import QuickBlock from '../QuickBlock' 7 | import messages from './Messages' 8 | import './BurndownChartBlock.css' 9 | 10 | const descriptor = { 11 | blockKey: 'BurndownChartBlock', 12 | label: messages.label, 13 | targets: [DashboardDataTarget.challenges, DashboardDataTarget.challenge], 14 | minWidth: 3, 15 | defaultWidth: 4, 16 | defaultHeight: 12, 17 | } 18 | 19 | export class BurndownChartBlock extends Component { 20 | render() { 21 | return ( 22 | 27 | }> 28 | 29 | 30 | ) 31 | } 32 | } 33 | 34 | registerBlockType(BurndownChartBlock, descriptor) 35 | -------------------------------------------------------------------------------- /src/services/Server/Route.test.js: -------------------------------------------------------------------------------- 1 | import Route from './Route' 2 | 3 | const base = 'http://localhost' 4 | const baseWithPort = `${base}:9000` 5 | const api = '2' 6 | 7 | describe('url', () => { 8 | it("includes the base url given at construction", () => { 9 | const route = new Route(base, api, 'somepath') 10 | expect(route.url()).toMatch(base) 11 | }) 12 | 13 | it("includes the api version given at construction after the base", () => { 14 | const route = new Route(base, api, 'somepath') 15 | expect(route.url()).toMatch(`${base}/api/${api}`) 16 | }) 17 | 18 | it("includes the port number if included in the base url", () => { 19 | const route = new Route(baseWithPort, api, 'somepath') 20 | expect(route.url()).toMatch(baseWithPort) 21 | }) 22 | 23 | it("substitutes variables into the path", () => { 24 | const route = new Route(baseWithPort, api, 'path/:first/path/:second') 25 | expect(route.url({first: 'hello', second: 'world'})).toMatch('path/hello/path/world') 26 | }) 27 | 28 | it("appends query params to the end", () => { 29 | const route = new Route(baseWithPort, api, 'path/:first/path/:second') 30 | expect(route.url( 31 | {first: 'hello', second: 'world'}, 32 | {page: 3, limit: 10} 33 | )).toMatch('path/hello/path/world?limit=10&page=3') 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/EnhancedMap/MapPane/MapPane.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import WithErrors from '../../HOCs/WithErrors/WithErrors' 4 | import AppErrors from '../../../services/Error/AppErrors' 5 | import './MapPane.css' 6 | 7 | /** 8 | * MapPane is a thin wrapper around map components that primarily serves as a 9 | * convenient boundary for CSS styling. 10 | * 11 | * @author [Neil Rotstan](https://github.com/nrotstan) 12 | */ 13 | export class MapPane extends Component { 14 | state = {hasError: false} 15 | 16 | componentDidCatch(error, info) { 17 | this.setState({hasError: true}) 18 | this.props.addError(AppErrors.map.renderFailure) 19 | } 20 | 21 | render() { 22 | if (this.state.hasError) { 23 | return ( 24 |
25 |
26 | 27 |
28 |
29 | ) 30 | } 31 | 32 | const childrenWithProps = React.Children.map( 33 | this.props.children, 34 | child => React.cloneElement(child, {...this.props}) 35 | ) 36 | 37 | return
{childrenWithProps}
38 | } 39 | } 40 | 41 | export default WithErrors(MapPane) 42 | -------------------------------------------------------------------------------- /src/services/Challenge/ChallengeDifficulty/ChallengeDifficulty.js: -------------------------------------------------------------------------------- 1 | import _map from 'lodash/map' 2 | import _fromPairs from 'lodash/fromPairs' 3 | import _isNumber from 'lodash/isNumber' 4 | import messages from './Messages' 5 | 6 | // These constants are defined on the server 7 | export const CHALLENGE_DIFFICULTY_EASY = 1 8 | export const CHALLENGE_DIFFICULTY_NORMAL = 2 9 | export const CHALLENGE_DIFFICULTY_EXPERT = 3 10 | 11 | export const ChallengeDifficulty = Object.freeze({ 12 | easy: CHALLENGE_DIFFICULTY_EASY, 13 | normal: CHALLENGE_DIFFICULTY_NORMAL, 14 | expert: CHALLENGE_DIFFICULTY_EXPERT, 15 | }) 16 | 17 | /** 18 | * Returns an object mapping difficulty values to raw internationalized 19 | * messages suitable for use with FormattedMessage or formatMessage. 20 | */ 21 | export const messagesByDifficulty = _fromPairs( 22 | _map(messages, (message, key) => [ChallengeDifficulty[key], message]) 23 | ) 24 | 25 | /** Returns object containing localized labels */ 26 | export const difficultyLabels = intl => _fromPairs( 27 | _map(messages, (message, key) => [key, intl.formatMessage(message)]) 28 | ) 29 | 30 | export const challengePassesDifficultyFilter = function(filter, challenge, props) { 31 | if (_isNumber(filter.difficulty)) { 32 | return challenge.difficulty === filter.difficulty 33 | } 34 | 35 | return true 36 | } 37 | -------------------------------------------------------------------------------- /src/services/Activity/ActivityItemTypes/ActivityItemTypes.js: -------------------------------------------------------------------------------- 1 | import _map from 'lodash/map' 2 | import _invert from 'lodash/invert' 3 | import _fromPairs from 'lodash/fromPairs' 4 | import messages from './Messages' 5 | 6 | // Constants defined on the server 7 | export const ITEM_TYPE_PROJECT = 0 8 | export const ITEM_TYPE_CHALLENGE = 1 9 | export const ITEM_TYPE_TASK = 2 10 | export const ITEM_TYPE_TAG = 3 11 | export const ITEM_TYPE_SURVEY = 4 12 | export const ITEM_TYPE_USER = 5 13 | export const ITEM_TYPE_GROUP = 6 14 | 15 | export const ActivityItemType = Object.freeze({ 16 | project: ITEM_TYPE_PROJECT, 17 | challenge: ITEM_TYPE_CHALLENGE, 18 | task: ITEM_TYPE_TASK, 19 | tag: ITEM_TYPE_TAG, 20 | survey: ITEM_TYPE_SURVEY, 21 | user: ITEM_TYPE_USER, 22 | group: ITEM_TYPE_GROUP, 23 | }) 24 | 25 | export const keysByType = Object.freeze(_invert(ActivityItemType)) 26 | 27 | /** 28 | * Returns an object mapping difficulty values to raw internationalized 29 | * messages suitable for use with FormattedMessage or formatMessage. 30 | */ 31 | export const messagesByType = _fromPairs( 32 | _map(messages, (message, key) => [ActivityItemType[key], message]) 33 | ) 34 | 35 | /** Returns object containing localized labels */ 36 | export const typeLabels = intl => _fromPairs( 37 | _map(messages, (message, key) => [key, intl.formatMessage(message)]) 38 | ) 39 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/GridBlocks/RecentActivityBlock/RecentActivityBlock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import _get from 'lodash/get' 4 | import _takeRight from 'lodash/takeRight' 5 | import { DashboardDataTarget } from '../../../../../services/Dashboard/Dashboard' 6 | import { registerBlockType } from '../BlockTypes' 7 | import ChallengeActivityTimeline 8 | from '../../../../ActivityTimeline/ChallengeActivityTimeline/ChallengeActivityTimeline' 9 | import QuickBlock from '../QuickBlock' 10 | import messages from './Messages' 11 | 12 | const descriptor = { 13 | blockKey: 'RecentActivityBlock', 14 | label: messages.label, 15 | targets: [DashboardDataTarget.challenge], 16 | minWidth: 3, 17 | defaultWidth: 4, 18 | defaultHeight: 14, 19 | } 20 | 21 | export class RecentActivityBlock extends Component { 22 | render() { 23 | return ( 24 | }> 27 | 30 | 31 | ) 32 | } 33 | } 34 | 35 | registerBlockType(RecentActivityBlock, descriptor) 36 | -------------------------------------------------------------------------------- /src/components/CommentList/CommentList.scss: -------------------------------------------------------------------------------- 1 | @import '../../variables.scss'; 2 | @import '../../mixins.scss'; 3 | 4 | .comment-list { 5 | &.none { 6 | font-size: $size-7; 7 | color: $grey; 8 | } 9 | 10 | &__comment { 11 | margin-bottom: 1em; 12 | 13 | &__header { 14 | display: flex; 15 | justify-content: flex-start; 16 | align-items: center; 17 | margin-bottom: 5px; 18 | } 19 | 20 | &__author { 21 | color: $grey; 22 | font-size: $size-7; 23 | font-weight: $weight-semibold; 24 | margin-right: 1em; 25 | } 26 | 27 | &__published-at { 28 | color: $grey-light; 29 | font-size: $size-8; 30 | font-weight: $weight-semibold; 31 | 32 | .time-part { 33 | margin-right: 0.5em; 34 | } 35 | } 36 | 37 | &__content { 38 | font-size: $size-7; 39 | background-color: $grey-lightest-more; 40 | border-radius: $radius-medium; 41 | padding: 1em; 42 | 43 | p { 44 | font-size: $size-7; 45 | } 46 | } 47 | 48 | &__meta { 49 | font-size: $size-8; 50 | color: $grey; 51 | display: flex; 52 | justify-content: space-between; 53 | 54 | a { 55 | color: $grey; 56 | } 57 | } 58 | 59 | .with-triangle-border { 60 | @include triangle-up(10px, $grey-lightest-more) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/components/TaskPane/TaskLocationMap/TaskLocationMap.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import PropTypes from 'prop-types' 3 | import L from 'leaflet' 4 | import 'leaflet-vectoricon' 5 | import InsetMap from '../../InsetMap/InsetMap' 6 | import WithTaskCenterPoint from '../../HOCs/WithTaskCenterPoint/WithTaskCenterPoint' 7 | import './TaskLocationMap.css' 8 | 9 | const starIconSvg = L.vectorIcon({ 10 | className: 'star-marker-icon', 11 | svgHeight: 20, 12 | svgWidth: 20, 13 | type: 'path', 14 | shape: { 15 | d: "M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" 16 | }, 17 | style: { 18 | fill: '#2281C2', // cornflower blue 19 | }, 20 | }) 21 | 22 | export class TaskLocationMap extends Component { 23 | render() { 24 | return ( 25 |
26 | 28 | 29 | 32 |
33 | ) 34 | } 35 | } 36 | 37 | TaskLocationMap.propTypes = { 38 | task: PropTypes.object, 39 | } 40 | 41 | export default WithTaskCenterPoint(TaskLocationMap) 42 | -------------------------------------------------------------------------------- /src/components/AdminPane/Manage/ChallengeFilterGroup/ChallengeFilterGroup.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { FormattedMessage } from 'react-intl' 3 | import { messagesByFilter } 4 | from '../../../../services/Dashboard/ChallengeFilter/ChallengeFilter' 5 | import DashboardFilterToggle 6 | from '../DashboardFilterToggle/DashboardFilterToggle' 7 | 8 | const VisibleFilterToggle = DashboardFilterToggle('challenge', 'visible') 9 | const PinnedFilterToggle = DashboardFilterToggle('challenge', 'pinned') 10 | 11 | export default class ChallengeFilterGroup extends Component { 12 | render() { 13 | return ( 14 | 15 | } /> 19 | } /> 23 | 24 | ) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/services/Dashboard/ProjectFilter/ProjectFilter.js: -------------------------------------------------------------------------------- 1 | import _fromPairs from 'lodash/fromPairs' 2 | import _map from 'lodash/map' 3 | import messages from './Messages' 4 | 5 | export const PROJECT_FILTER_VISIBLE = 'visible' 6 | export const PROJECT_FILTER_OWNER = 'owner' 7 | export const PROJECT_FILTER_PINNED = 'pinned' 8 | 9 | export const ProjectFilter = { 10 | visible: PROJECT_FILTER_VISIBLE, 11 | owner: PROJECT_FILTER_OWNER, 12 | pinned: PROJECT_FILTER_PINNED, 13 | } 14 | 15 | export const defaultProjectFilters = function() { 16 | return { 17 | [PROJECT_FILTER_VISIBLE]: false, 18 | [PROJECT_FILTER_OWNER]: false, 19 | [PROJECT_FILTER_PINNED]: false, 20 | } 21 | } 22 | 23 | export const projectPassesFilters = function(project, manager, pins, projectFilters) { 24 | if (projectFilters.visible && !project.enabled) { 25 | return false 26 | } 27 | 28 | if (projectFilters.owner && !manager.isProjectOwner(project)) { 29 | return false 30 | } 31 | 32 | if (projectFilters.pinned && pins.indexOf(project.id) === -1) { 33 | return false 34 | } 35 | 36 | return true 37 | } 38 | 39 | /** 40 | * Returns an object mapping project filters to raw internationalized 41 | * messages suitable for use with FormattedMessage or formatMessage. 42 | */ 43 | export const messagesByFilter = _fromPairs( 44 | _map(messages, (message, key) => [ProjectFilter[key], message]) 45 | ) 46 | -------------------------------------------------------------------------------- /src/components/HOCs/WithCommandInterpreter/WithCommandInterpreter.test.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import _findIndex from 'lodash/findIndex' 3 | import { executeCommand } from './WithCommandInterpreter' 4 | 5 | let basicProps = {} 6 | 7 | beforeEach(() => { 8 | basicProps.setSearch = jest.fn() 9 | basicProps.clearSearch = jest.fn() 10 | basicProps.updateChallengeSearchMapBounds = jest.fn() 11 | }) 12 | 13 | 14 | test("executeCommand recognizes s/", () => { 15 | //const wrapper = new (WithCommandInterpreter(
)) 16 | 17 | executeCommand(basicProps, "s/hello world") 18 | expect(basicProps.setSearch).toHaveBeenCalledWith("hello world") 19 | }) 20 | 21 | test("executeCommand recognizes m/ with 4 bounds", () => { 22 | //const wrapper = new (WithCommandInterpreter(
)) 23 | 24 | executeCommand(basicProps, "m/1.1,2.2,3.3,4.4") 25 | expect(basicProps.setSearch).not.toHaveBeenCalled() 26 | expect(basicProps.updateChallengeSearchMapBounds).toHaveBeenCalledWith([1.1, 2.2, 3.3, 4.4], true) 27 | }) 28 | 29 | test("executeCommand recognizes m/ with 2 bounds as centerpoint", () => { 30 | //const wrapper = new (WithCommandInterpreter(
)) 31 | 32 | executeCommand(basicProps, "m/1,4") 33 | expect(basicProps.setSearch).not.toHaveBeenCalled() 34 | expect(basicProps.updateChallengeSearchMapBounds).toHaveBeenCalledWith([0.625, 3.625, 1.375, 4.375], true) 35 | }) 36 | -------------------------------------------------------------------------------- /src/components/TaskPane/ActiveTaskDetails/ActiveTaskControls/TaskCompletionStep1/TaskCompletionStep1.scss: -------------------------------------------------------------------------------- 1 | @import '../../../../../variables.scss'; 2 | 3 | .task-edit-controls { 4 | &__control-block { 5 | display: flex; 6 | justify-content: space-between; 7 | flex-wrap: wrap; 8 | 9 | button.button.large-and-wide { 10 | margin-bottom: 10px; 11 | 12 | .control-icon svg { 13 | height: 20px; 14 | width: auto; 15 | margin-right: 10px; 16 | } 17 | } 18 | } 19 | 20 | &__task-comment { 21 | width: 100%; 22 | margin-bottom: 20px; 23 | 24 | input { 25 | width: 100%; 26 | font-size: 16px; 27 | padding: 5px; 28 | border: 2px solid $grey; 29 | border-radius: $radius-medium; 30 | } 31 | } 32 | 33 | &.is-minimized { 34 | .task-edit-controls__control-block { 35 | flex-direction: column; 36 | 37 | .control-label { 38 | display: none; 39 | } 40 | } 41 | 42 | button { 43 | margin-bottom: 5px; 44 | } 45 | 46 | .task-edit-controls__icon { 47 | height: 26px; 48 | width: auto; 49 | margin-left: 0px; 50 | margin-right: 0px; 51 | fill: $default-icon-color; 52 | } 53 | 54 | .task-edit-controls__task-comment { 55 | display: none; 56 | } 57 | 58 | .control-label { 59 | display: none; 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/components/HOCs/WithProjects/WithProjects.test.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { denormalize } from 'normalizr' 3 | import { mapStateToProps, mapDispatchToProps } from './WithProjects' 4 | import { fetchProjectChallenges } 5 | from '../../../services/Challenge/Challenge' 6 | 7 | jest.mock('normalizr') 8 | jest.mock('../../../services/Challenge/Challenge') 9 | 10 | denormalize.mockImplementation((project, schema, entities) => project) 11 | 12 | let basicState = null 13 | 14 | beforeEach(() => { 15 | basicState = { 16 | entities: { 17 | projects: [ 18 | { 19 | id: "123", 20 | name: "first", 21 | }, 22 | { 23 | id: "456", 24 | name: "second", 25 | } 26 | ], 27 | } 28 | } 29 | }) 30 | 31 | test("mapStateToProps maps projects", () => { 32 | const mappedProps = mapStateToProps(basicState) 33 | expect(mappedProps.projects).toEqual(basicState.entities.projects) 34 | 35 | expect(mappedProps).toMatchSnapshot() 36 | }) 37 | 38 | test("mapDispatchToProps maps function fetchProjectChallenges", () => { 39 | const dispatch = jest.fn(() => Promise.resolve()) 40 | const mappedProps = mapDispatchToProps(dispatch, {}) 41 | 42 | mappedProps.fetchProjectChallenges("someProjectId") 43 | expect(dispatch).toBeCalled() 44 | expect(fetchProjectChallenges).toBeCalledWith("someProjectId") 45 | }) 46 | -------------------------------------------------------------------------------- /src/services/Dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import uuidv4 from 'uuid/v4' 2 | import _isFinite from 'lodash/isFinite' 3 | 4 | export const CURRENT_DATAMODEL_VERSION=1 5 | 6 | export const DASHBOARD_DATA_TARGET_PROJECTS = 'projects' 7 | export const DASHBOARD_DATA_TARGET_PROJECT = 'project' 8 | export const DASHBOARD_DATA_TARGET_CHALLENGES = 'challenges' 9 | export const DASHBOARD_DATA_TARGET_CHALLENGE = 'challenge' 10 | export const DASHBOARD_DATA_TARGET_TASKS = 'tasks' 11 | export const DASHBOARD_DATA_TARGET_TASK = 'task' 12 | 13 | export const DashboardDataTarget = { 14 | projects: DASHBOARD_DATA_TARGET_PROJECTS, 15 | project: DASHBOARD_DATA_TARGET_PROJECT, 16 | challenges: DASHBOARD_DATA_TARGET_CHALLENGES, 17 | challenge: DASHBOARD_DATA_TARGET_CHALLENGE, 18 | tasks: DASHBOARD_DATA_TARGET_TASKS, 19 | task: DASHBOARD_DATA_TARGET_TASK, 20 | } 21 | 22 | export const generateDashboardId = function() { 23 | return uuidv4() 24 | } 25 | 26 | export const migrateDashboard = function(configuration, generateDefaultConfiguration) { 27 | // Dashboards lacking any version number cannot be migrated. Reset to 28 | // default configuration. 29 | if (!_isFinite(configuration.dataModelVersion)) { 30 | return Object.assign(generateDefaultConfiguration(), { 31 | id: configuration.id, 32 | label: configuration.label, 33 | }) 34 | } 35 | 36 | // No migrations yet 37 | return configuration 38 | } 39 | --------------------------------------------------------------------------------