From 7e4ab0cdd5a0c8528b45781522d210acd81cff36 Mon Sep 17 00:00:00 2001 From: Migsar Date: Wed, 14 Aug 2019 11:27:59 +0100 Subject: [PATCH] feat(NA): First mobile version (#48) * feat(NA): Add redux. * feat(NA): Add redux-first-router. * feat(#1): Add generic MapPanel and Map components * fix(#1): Clean and adapt map component. * feat(#5): Add state to url. * feat(#6): Create global settings state. * feat(NA): Add eslit with vizzuality rules. * fix(NA): Update eslint to 5.16.0 as required by react-scripts and replace node_path with baseUrl in jsconfig.json * feat(NA): PT#166232015 Location Selector. * feat(#7): PT#166232052 Dashboard. * Added .nvmrc file * added .editorconfig file, in order to write code in the same way * removed eslintConfig because we already have eslintrc file * removed peer dependency * feat(#10): Create and connect dashboard component * feat(#11): Create dashboard schema. * feat(#12): Create search schema. * chore(NA): Comment out app state to url for now. * fix(#11): Change title to location. * feat(#15): Service for datasets. * feat(#16): Add layer manager. * feat(#18): Add map navigation. * fix(#18): Linter fixes. * feat(#17): Create base Widget component. * feat(#17): Add basic style to Widget component. * feat(#17): Connect widget and dashboard state. * added readme and env sample * node version for heroku * node version for heroku * updated README with heroku deploy docs * feat(#17): Make widget layers actionable. * adding first styles and layout * adding static server based on express for heroku * styles for widget card * feat(#17): Create base Widget component. * feat(#17): Add basic style to Widget component. * feat(#17): Connect widget and dashboard state. * feat(#17): Make widget layers actionable. * chore(NA) Disable state to url for map interaction speed. * changed typo for widget * add chart component * add mangrove coverage widget] * add mangroves base * migrated to scss modules * chart styles * use sass * net change widget * removed no nedded dependencies * connected with mapbox service * connecting widgets and collapse * basic sentence * basic label structure * rm react-select, not using atm * update comments * merging widgets * fixed mangrove activity slug * disabled url update * dashboard removed * toggle and layers * fixed location selector * updated styles for widget * added OpenSans font * map layers and styles * changed thumbnails for basemap selector * adding and removing layers * added and removing layer using mapStyle object * add and remove layer * added favicon * fixed templates * connected widget data * details and style for widgets and dashboard * header and location selector * location styles * navigation using location selector * connect current location with locations * fixed prop types for legend * improved performance for map * added fly to any location * location navigation * disabling redux for production * widget animation * styles for widgets * mangrove coverage adjustments * micro optimization for locations sagas * fixed fly to location * fixed location search * added conservation hotspots widget and disable button for widgets without layer * added link for download raw data * download csv * styles for download link * changed widget for net change * connected data with net change widget for tanzania * fixed mangrove net change for wordwide * styles for select * changed template for widgets * added support for multiple layers * added button for download data for net change widget * added interactivity for net change * fix some bugs and style improvements * added autofocus for search input * font size depending on title length * added padding for fitbounds * fixed axis y for mangrove net widget * added transifex live code and changed typo in title * added notranslate classnames to avoid transifex detection * added more span for translation * storybook config * comments removed * widgets stories * basemap selector stories * knobs added * knobs rule added to eslint * Select stories * legend-item stories * CDN global styles added to storybook config * preview and manager head tags * fixed dependencies conflicts * open search modal clicking on the title * created modal wrapper using react modal * fixing location selector modal when current location doesnt exist * fixing number format for charts * storybook config addons * button stories * map stories * header stories * chart stories * select stories * widget stories * global variables * button stories * button group * files deleted * button group component * storybook dependencies and added global styles * stoorybook provider * modal stories * location-modal stories * button group stories * map stories * widget download data link to component * map temp container * spinner to stoorybook * arrow in select + context * arrow select * spinner * language-selector * arrow select * collapse widgets btn styles * header elements swapped * hotspots included in modal * select cursor pointer * arrow in select styled * dropdown arrow * open select removed * btn styles on active bar * option button styles * dashboard component for scroll * PR fixes * PR - #31 amendments * language select * spinner fix * disable options * dates disabled styles * CHANGELOG file * transifex WIP * sidebar instead dashbaord * sticky header styles and sidebar component * transifex index + store * transifex WIP * sidebar width * changelog just for two weeks * eslint fix * z-index changed in location modal * removing file * language sagas * language select component * functionality to language selct * preselected language styles * feat(NA): Url to state feature. * fix(NA): Fixing error when no query was present. * feat(NA): Add delay so restoring widget isActive works. * changelog updated * link added to hotspot card * refactor(NA): Change from mapbox datasets to api (#43) * changed locations * locations integration from api * adde data for dashboards, widgets and layers * locations with geom * using data instead fetch * connected data * changes * coverage * mangrove data * connected widgets * widget api fix * mangrove coverage integrated * mangrove coverage migrated * fix(NA): Change Back to worldwide id from global to worldwide. * Feature/mobile (#38) * react responsive and main layout * view map btn component * media queries and mobile view styles * media queries adjusted in header * mobile condition to redux, btn and header styles adapted * action bar in header * hotspots widget mobile * typo error * indentation * background, widgets arrow * desktop styles fixed * view map btn and active layers * desktop layout component * mobile layout component * layers collapsed in map view * mediaqueries added * unused dependencies removed * mediaqueries fix * map btn functionality * legend collapse * import styles using SASS_PATH * legend item back * map legend * conflict and legend styles * PR changes * modal styles * PR - changes * Fix/fixed header (#45) * react responsive and main layout * view map btn component * media queries and mobile view styles * media queries adjusted in header * mobile condition to redux, btn and header styles adapted * action bar in header * hotspots widget mobile * typo error * indentation * background, widgets arrow * desktop styles fixed * view map btn and active layers * desktop layout component * mobile layout component * layers collapsed in map view * mediaqueries added * unused dependencies removed * mediaqueries fix * map btn functionality * legend collapse * import styles using SASS_PATH * legend item back * map legend * conflict and legend styles * PR changes * modal styles * PR - changes * header and modal fix * Fix/fixed header (#46) * react responsive and main layout * view map btn component * media queries and mobile view styles * media queries adjusted in header * mobile condition to redux, btn and header styles adapted * action bar in header * hotspots widget mobile * typo error * indentation * background, widgets arrow * desktop styles fixed * view map btn and active layers * desktop layout component * mobile layout component * layers collapsed in map view * mediaqueries added * unused dependencies removed * mediaqueries fix * map btn functionality * legend collapse * import styles using SASS_PATH * legend item back * map legend * conflict and legend styles * PR changes * modal styles * PR - changes * header and modal fix * mobile modal btn * Feature/widget alerts (#44) * changed locations * locations integration from api * adde data for dashboards, widgets and layers * locations with geom * using data instead fetch * connected data * changes * coverage * mangrove data * connected widgets * widget api fix * mangrove coverage integrated * mangrove coverage migrated * fix(NA): Change Back to worldwide id from global to worldwide. * feat(NA): Basic layout for alerts widget. * refactor: Refactor WidgetList and Widget to be more clear. * fix(NA): Layer toggle was not working after API refactor. * refactor(NA): Start refactoring chart component. * fix(NA): Requested changes. * widget conected (#47) * widget conected * net change widget data * extra file removed --- .editorconfig | 16 + .env.sample | 4 + .eslintrc.json | 24 + .nvmrc | 1 + .storybook/addons.js | 1 + .storybook/config.js | 15 + .storybook/contexts.js | 22 + .storybook/preview-head.html | 3 + CHANGELOG.md | 51 + Procfile | 1 + README.md | 46 +- jsconfig.json | 5 + package.json | 67 +- public/android-chrome-192x192.png | Bin 0 -> 8121 bytes public/android-chrome-512x512.png | Bin 0 -> 8537 bytes public/apple-touch-icon.png | Bin 0 -> 2163 bytes public/browserconfig.xml | 9 + public/favicon-16x16.png | Bin 0 -> 596 bytes public/favicon-32x32.png | Bin 0 -> 896 bytes public/favicon.ico | Bin 3870 -> 15086 bytes public/index.html | 17 +- public/mstile-150x150.png | Bin 0 -> 2596 bytes public/safari-pinned-tab.svg | 30 + public/site.webmanifest | 19 + server.js | 11 + src/App.css | 33 - src/App.js | 26 - src/App.test.js | 9 - src/components/basemap-selector/component.js | 71 + src/components/basemap-selector/constants.js | 16 + src/components/basemap-selector/index.js | 16 + src/components/basemap-selector/stories.jsx | 14 + .../basemap-selector/style.module.scss | 60 + .../basemap-selector/thumbs/btn-dark@2x.png | Bin 0 -> 7979 bytes .../basemap-selector/thumbs/btn-light@2x.png | Bin 0 -> 7741 bytes .../thumbs/btn-satellite@2x.png | Bin 0 -> 11851 bytes .../basemap-selector/thumbs/dark.png | Bin 0 -> 2229 bytes .../basemap-selector/thumbs/light.png | Bin 0 -> 1590 bytes .../basemap-selector/thumbs/satellite.jpeg | Bin 0 -> 1625 bytes src/components/button/component.js | 30 + src/components/button/index.js | 3 + src/components/button/stories.jsx | 46 + src/components/button/style.module.scss | 61 + src/components/buttonGroup/component.js | 12 + src/components/buttonGroup/index.js | 3 + src/components/buttonGroup/stories.jsx | 13 + src/components/buttonGroup/style.module.scss | 27 + src/components/chart/component.js | 283 + src/components/chart/constants.js | 11 + src/components/chart/index.js | 1 + .../chart/rechart-components/defaults.js | 0 .../chart/rechart-components/index.js | 38 + src/components/chart/stories.jsx | 15 + src/components/chart/style.module.scss | 4 + src/components/chart/tick/component.js | 62 + src/components/chart/tick/index.js | 3 + src/components/chart/tooltip/component.js | 75 + src/components/chart/tooltip/index.js | 3 + src/components/chart/tooltip/style.module.css | 46 + src/components/datepicker/component.js | 71 + src/components/datepicker/index.js | 1 + src/components/datepicker/input/component.js | 42 + src/components/datepicker/input/index.js | 1 + .../datepicker/input/style.module.scss | 33 + src/components/datepicker/style.module.scss | 16 + src/components/header/bg-fixed.svg | 25 + src/components/header/bg-shape.svg | 34 + src/components/header/component.js | 86 + src/components/header/index.js | 14 + src/components/header/stories.jsx | 9 + src/components/header/style.module.scss | 98 + src/components/language-selector/component.js | 88 + src/components/language-selector/index.js | 15 + src/components/language-selector/stories.jsx | 13 + .../language-selector/style.module.scss | 26 + src/components/layout/desktop/component.js | 18 + src/components/layout/desktop/index.js | 3 + src/components/layout/mobile/component.js | 33 + src/components/layout/mobile/index.js | 8 + src/components/layout/style.module.scss | 10 + src/components/link/component.js | 34 + src/components/link/index.js | 3 + src/components/link/stories.jsx | 13 + src/components/link/style.module.scss | 9 + src/components/location-modal/component.js | 162 + src/components/location-modal/index.js | 17 + src/components/location-modal/stories.jsx | 11 + .../location-modal/style.module.scss | 133 + src/components/map-legend/component.js | 20 + src/components/map-legend/index.js | 15 + .../map-legend/legend-item/component.js | 35 + .../map-legend/legend-item/index.js | 14 + .../map-legend/legend-item/stories.jsx | 16 + .../map-legend/legend-item/style.module.scss | 30 + src/components/map-legend/mobile/component.js | 47 + src/components/map-legend/mobile/index.js | 13 + .../map-legend/mobile/style.module.scss | 39 + .../map-legend/mobile/style.module.scss~HEAD | 39 + src/components/map/component.js | 98 + src/components/map/index.js | 18 + src/components/map/stories.jsx | 12 + src/components/map/style.module.scss | 44 + src/components/modal/component.js | 24 + src/components/modal/index.js | 3 + src/components/modal/stories.jsx | 10 + src/components/modal/styles.scss | 19 + src/components/pages/component.js | 29 + src/components/pages/index.js | 6 + src/components/select/component.js | 56 + src/components/select/index.js | 3 + src/components/select/stories.jsx | 26 + src/components/select/style.js | 117 + src/components/select/style.module.scss | 6 + src/components/sidebar/component.js | 65 + src/components/sidebar/index.js | 17 + src/components/sidebar/style.module.scss | 29 + src/components/spinner/component.js | 24 + src/components/spinner/index.js | 3 + src/components/spinner/stories.jsx | 16 + src/components/spinner/style.module.scss | 27 + src/components/view-selector/component.js | 41 + src/components/view-selector/index.js | 15 + src/components/view-selector/stories.jsx | 14 + .../view-selector/style.module.scss | 42 + src/components/widget/component.js | 139 + src/components/widget/index.js | 1 + src/components/widget/legend/component.js | 42 + src/components/widget/legend/index.js | 3 + .../widget/legend/style.module.scss | 67 + src/components/widget/stories.jsx | 113 + src/components/widget/style.module.scss | 108 + src/components/widget/templates/configs.js | 19 + .../templates/highlighted-places/component.js | 35 + .../templates/highlighted-places/config.js | 13 + .../templates/highlighted-places/index.js | 3 + .../highlighted-places/style.module.scss | 71 + src/components/widget/templates/index.js | 13 + .../templates/mangrove-activity/component.js | 96 + .../templates/mangrove-activity/config.js | 100 + .../templates/mangrove-activity/index.js | 12 + .../templates/mangrove-alerts/component.js | 76 + .../templates/mangrove-alerts/config.js | 190 + .../widget/templates/mangrove-alerts/index.js | 1 + .../templates/mangrove-coverage/component.js | 105 + .../templates/mangrove-coverage/config.js | 96 + .../templates/mangrove-coverage/index.js | 1 + .../mangrove-net-change/component.js | 103 + .../templates/mangrove-net-change/config.js | 117 + .../templates/mangrove-net-change/index.js | 3 + src/components/widget/tooltip/component.js | 80 + src/components/widget/tooltip/index.js | 3 + .../widget/tooltip/style.module.scss | 47 + src/components/widgets/component.js | 52 + src/components/widgets/index.js | 23 + src/components/widgets/stories.jsx | 12 + src/components/widgets/style.module.scss | 5 + src/config/data.json | 96 + src/config/router.js | 26 + src/config/sagas.js | 28 + src/config/store.js | 56 + src/index.css | 13 - src/index.js | 20 +- src/logo.svg | 7 - src/modules/app/actions.js | 7 + src/modules/app/index.js | 5 + src/modules/app/initial-state.js | 5 + src/modules/app/reducers.js | 11 + src/modules/dashboards/actions.js | 6 + src/modules/dashboards/index.js | 6 + src/modules/dashboards/initial-state.js | 6 + src/modules/dashboards/reducers.js | 19 + src/modules/dashboards/sagas.js | 12 + src/modules/dashboards/selectors.js | 17 + src/modules/languages/actions.js | 8 + src/modules/languages/index.js | 6 + src/modules/languages/initial-state.js | 6 + src/modules/languages/reducers.js | 23 + src/modules/languages/sagas.js | 26 + src/modules/layers/actions.js | 9 + src/modules/layers/index.js | 6 + src/modules/layers/initial-state.js | 6 + src/modules/layers/reducers.js | 34 + src/modules/layers/sagas.js | 17 + src/modules/layers/selectors.js | 17 + src/modules/locations/actions.js | 10 + src/modules/locations/index.js | 6 + src/modules/locations/initial-state.js | 8 + src/modules/locations/reducers.js | 29 + src/modules/locations/sagas.js | 19 + src/modules/locations/selectors.js | 35 + src/modules/mangrove-data/actions.js | 6 + src/modules/mangrove-data/index.js | 6 + src/modules/mangrove-data/initial-state.js | 6 + src/modules/mangrove-data/reducers.js | 20 + src/modules/mangrove-data/sagas.js | 20 + src/modules/mangrove-data/selectors.js | 0 src/modules/map-styles/actions.js | 6 + src/modules/map-styles/index.js | 6 + src/modules/map-styles/initial-state.js | 5 + src/modules/map-styles/reducers.js | 27 + src/modules/map-styles/sagas.js | 19 + src/modules/map-styles/selectors.js | 34 + src/modules/map-styles/style-manager.js | 54 + .../map-styles/templates/basemaps/dark.json | 243 + .../map-styles/templates/basemaps/light.json | 281 + .../templates/basemaps/satellite.json | 15 + .../templates/contextuals/countries.json | 41 + .../templates/contextuals/wdpa.json | 67 + src/modules/map-styles/templates/index.js | 19 + .../map-styles/templates/template.json | 57 + src/modules/map/actions.js | 5 + src/modules/map/index.js | 34 + src/modules/map/initial-state.js | 12 + src/modules/map/reducers.js | 24 + src/modules/map/sagas.js | 56 + src/modules/pages/actions.js | 12 + src/modules/pages/constants.js | 31 + src/modules/pages/index.js | 6 + src/modules/pages/initial-state.js | 3 + src/modules/pages/reducers.js | 10 + src/modules/pages/sagas.js | 39 + src/modules/widgets/actions.js | 14 + src/modules/widgets/index.js | 56 + src/modules/widgets/initial-state.js | 5 + src/modules/widgets/reducers.js | 66 + src/modules/widgets/sagas.js | 69 + src/modules/widgets/selectors.js | 89 + src/pages/app/bg-fixed.svg | 25 + src/pages/app/bg-shape.svg | 34 + src/pages/app/index.js | 22 + src/pages/app/style.module.scss | 37 + src/pages/not-found/index.js | 5 + src/serviceWorker.js | 135 - src/services/api-service.js | 33 + src/services/dataset-service.js | 40 + src/services/style-service.js | 36 + src/styles/_vars.scss | 58 + src/styles/lib/_react-datepicker.scss | 134 + src/styles/main.scss | 41 + src/utils/jsonParsers.js | 14 + src/utils/nice-date.js | 13 + src/utils/query-state/constants.js | 8 + src/utils/query-state/index.js | 121 + src/utils/query-state/stateToUrl.js | 28 + src/utils/responsive.js | 10 + src/utils/storybookProvider.js | 10 + yarn.lock | 5632 ++++++++++++++--- 247 files changed, 12474 insertions(+), 1160 deletions(-) create mode 100644 .editorconfig create mode 100644 .env.sample create mode 100644 .eslintrc.json create mode 100644 .nvmrc create mode 100644 .storybook/addons.js create mode 100644 .storybook/config.js create mode 100644 .storybook/contexts.js create mode 100644 .storybook/preview-head.html create mode 100644 CHANGELOG.md create mode 100644 Procfile create mode 100644 jsconfig.json create mode 100644 public/android-chrome-192x192.png create mode 100644 public/android-chrome-512x512.png create mode 100644 public/apple-touch-icon.png create mode 100644 public/browserconfig.xml create mode 100644 public/favicon-16x16.png create mode 100644 public/favicon-32x32.png create mode 100644 public/mstile-150x150.png create mode 100644 public/safari-pinned-tab.svg create mode 100644 public/site.webmanifest create mode 100644 server.js delete mode 100644 src/App.css delete mode 100644 src/App.js delete mode 100644 src/App.test.js create mode 100644 src/components/basemap-selector/component.js create mode 100644 src/components/basemap-selector/constants.js create mode 100644 src/components/basemap-selector/index.js create mode 100644 src/components/basemap-selector/stories.jsx create mode 100644 src/components/basemap-selector/style.module.scss create mode 100755 src/components/basemap-selector/thumbs/btn-dark@2x.png create mode 100755 src/components/basemap-selector/thumbs/btn-light@2x.png create mode 100755 src/components/basemap-selector/thumbs/btn-satellite@2x.png create mode 100644 src/components/basemap-selector/thumbs/dark.png create mode 100644 src/components/basemap-selector/thumbs/light.png create mode 100644 src/components/basemap-selector/thumbs/satellite.jpeg create mode 100644 src/components/button/component.js create mode 100644 src/components/button/index.js create mode 100644 src/components/button/stories.jsx create mode 100644 src/components/button/style.module.scss create mode 100644 src/components/buttonGroup/component.js create mode 100644 src/components/buttonGroup/index.js create mode 100644 src/components/buttonGroup/stories.jsx create mode 100644 src/components/buttonGroup/style.module.scss create mode 100644 src/components/chart/component.js create mode 100644 src/components/chart/constants.js create mode 100644 src/components/chart/index.js create mode 100644 src/components/chart/rechart-components/defaults.js create mode 100644 src/components/chart/rechart-components/index.js create mode 100644 src/components/chart/stories.jsx create mode 100644 src/components/chart/style.module.scss create mode 100644 src/components/chart/tick/component.js create mode 100644 src/components/chart/tick/index.js create mode 100644 src/components/chart/tooltip/component.js create mode 100644 src/components/chart/tooltip/index.js create mode 100644 src/components/chart/tooltip/style.module.css create mode 100644 src/components/datepicker/component.js create mode 100644 src/components/datepicker/index.js create mode 100644 src/components/datepicker/input/component.js create mode 100644 src/components/datepicker/input/index.js create mode 100644 src/components/datepicker/input/style.module.scss create mode 100644 src/components/datepicker/style.module.scss create mode 100644 src/components/header/bg-fixed.svg create mode 100644 src/components/header/bg-shape.svg create mode 100644 src/components/header/component.js create mode 100644 src/components/header/index.js create mode 100644 src/components/header/stories.jsx create mode 100644 src/components/header/style.module.scss create mode 100644 src/components/language-selector/component.js create mode 100644 src/components/language-selector/index.js create mode 100644 src/components/language-selector/stories.jsx create mode 100644 src/components/language-selector/style.module.scss create mode 100644 src/components/layout/desktop/component.js create mode 100644 src/components/layout/desktop/index.js create mode 100644 src/components/layout/mobile/component.js create mode 100644 src/components/layout/mobile/index.js create mode 100644 src/components/layout/style.module.scss create mode 100644 src/components/link/component.js create mode 100644 src/components/link/index.js create mode 100644 src/components/link/stories.jsx create mode 100644 src/components/link/style.module.scss create mode 100644 src/components/location-modal/component.js create mode 100644 src/components/location-modal/index.js create mode 100644 src/components/location-modal/stories.jsx create mode 100644 src/components/location-modal/style.module.scss create mode 100644 src/components/map-legend/component.js create mode 100644 src/components/map-legend/index.js create mode 100644 src/components/map-legend/legend-item/component.js create mode 100644 src/components/map-legend/legend-item/index.js create mode 100644 src/components/map-legend/legend-item/stories.jsx create mode 100644 src/components/map-legend/legend-item/style.module.scss create mode 100644 src/components/map-legend/mobile/component.js create mode 100644 src/components/map-legend/mobile/index.js create mode 100644 src/components/map-legend/mobile/style.module.scss create mode 100644 src/components/map-legend/mobile/style.module.scss~HEAD create mode 100644 src/components/map/component.js create mode 100644 src/components/map/index.js create mode 100644 src/components/map/stories.jsx create mode 100644 src/components/map/style.module.scss create mode 100644 src/components/modal/component.js create mode 100644 src/components/modal/index.js create mode 100644 src/components/modal/stories.jsx create mode 100644 src/components/modal/styles.scss create mode 100644 src/components/pages/component.js create mode 100644 src/components/pages/index.js create mode 100644 src/components/select/component.js create mode 100644 src/components/select/index.js create mode 100644 src/components/select/stories.jsx create mode 100644 src/components/select/style.js create mode 100644 src/components/select/style.module.scss create mode 100644 src/components/sidebar/component.js create mode 100644 src/components/sidebar/index.js create mode 100644 src/components/sidebar/style.module.scss create mode 100644 src/components/spinner/component.js create mode 100644 src/components/spinner/index.js create mode 100644 src/components/spinner/stories.jsx create mode 100644 src/components/spinner/style.module.scss create mode 100644 src/components/view-selector/component.js create mode 100644 src/components/view-selector/index.js create mode 100644 src/components/view-selector/stories.jsx create mode 100644 src/components/view-selector/style.module.scss create mode 100644 src/components/widget/component.js create mode 100644 src/components/widget/index.js create mode 100644 src/components/widget/legend/component.js create mode 100644 src/components/widget/legend/index.js create mode 100644 src/components/widget/legend/style.module.scss create mode 100644 src/components/widget/stories.jsx create mode 100644 src/components/widget/style.module.scss create mode 100644 src/components/widget/templates/configs.js create mode 100644 src/components/widget/templates/highlighted-places/component.js create mode 100644 src/components/widget/templates/highlighted-places/config.js create mode 100644 src/components/widget/templates/highlighted-places/index.js create mode 100644 src/components/widget/templates/highlighted-places/style.module.scss create mode 100644 src/components/widget/templates/index.js create mode 100644 src/components/widget/templates/mangrove-activity/component.js create mode 100644 src/components/widget/templates/mangrove-activity/config.js create mode 100644 src/components/widget/templates/mangrove-activity/index.js create mode 100644 src/components/widget/templates/mangrove-alerts/component.js create mode 100644 src/components/widget/templates/mangrove-alerts/config.js create mode 100644 src/components/widget/templates/mangrove-alerts/index.js create mode 100644 src/components/widget/templates/mangrove-coverage/component.js create mode 100644 src/components/widget/templates/mangrove-coverage/config.js create mode 100644 src/components/widget/templates/mangrove-coverage/index.js create mode 100644 src/components/widget/templates/mangrove-net-change/component.js create mode 100644 src/components/widget/templates/mangrove-net-change/config.js create mode 100644 src/components/widget/templates/mangrove-net-change/index.js create mode 100644 src/components/widget/tooltip/component.js create mode 100644 src/components/widget/tooltip/index.js create mode 100644 src/components/widget/tooltip/style.module.scss create mode 100644 src/components/widgets/component.js create mode 100644 src/components/widgets/index.js create mode 100644 src/components/widgets/stories.jsx create mode 100644 src/components/widgets/style.module.scss create mode 100644 src/config/data.json create mode 100644 src/config/router.js create mode 100644 src/config/sagas.js create mode 100644 src/config/store.js delete mode 100644 src/index.css delete mode 100644 src/logo.svg create mode 100644 src/modules/app/actions.js create mode 100644 src/modules/app/index.js create mode 100644 src/modules/app/initial-state.js create mode 100644 src/modules/app/reducers.js create mode 100644 src/modules/dashboards/actions.js create mode 100644 src/modules/dashboards/index.js create mode 100644 src/modules/dashboards/initial-state.js create mode 100644 src/modules/dashboards/reducers.js create mode 100644 src/modules/dashboards/sagas.js create mode 100644 src/modules/dashboards/selectors.js create mode 100644 src/modules/languages/actions.js create mode 100644 src/modules/languages/index.js create mode 100644 src/modules/languages/initial-state.js create mode 100644 src/modules/languages/reducers.js create mode 100644 src/modules/languages/sagas.js create mode 100644 src/modules/layers/actions.js create mode 100644 src/modules/layers/index.js create mode 100644 src/modules/layers/initial-state.js create mode 100644 src/modules/layers/reducers.js create mode 100644 src/modules/layers/sagas.js create mode 100644 src/modules/layers/selectors.js create mode 100644 src/modules/locations/actions.js create mode 100644 src/modules/locations/index.js create mode 100644 src/modules/locations/initial-state.js create mode 100644 src/modules/locations/reducers.js create mode 100644 src/modules/locations/sagas.js create mode 100644 src/modules/locations/selectors.js create mode 100644 src/modules/mangrove-data/actions.js create mode 100644 src/modules/mangrove-data/index.js create mode 100644 src/modules/mangrove-data/initial-state.js create mode 100644 src/modules/mangrove-data/reducers.js create mode 100644 src/modules/mangrove-data/sagas.js create mode 100644 src/modules/mangrove-data/selectors.js create mode 100644 src/modules/map-styles/actions.js create mode 100644 src/modules/map-styles/index.js create mode 100644 src/modules/map-styles/initial-state.js create mode 100644 src/modules/map-styles/reducers.js create mode 100644 src/modules/map-styles/sagas.js create mode 100644 src/modules/map-styles/selectors.js create mode 100644 src/modules/map-styles/style-manager.js create mode 100644 src/modules/map-styles/templates/basemaps/dark.json create mode 100644 src/modules/map-styles/templates/basemaps/light.json create mode 100644 src/modules/map-styles/templates/basemaps/satellite.json create mode 100644 src/modules/map-styles/templates/contextuals/countries.json create mode 100644 src/modules/map-styles/templates/contextuals/wdpa.json create mode 100644 src/modules/map-styles/templates/index.js create mode 100644 src/modules/map-styles/templates/template.json create mode 100644 src/modules/map/actions.js create mode 100644 src/modules/map/index.js create mode 100644 src/modules/map/initial-state.js create mode 100644 src/modules/map/reducers.js create mode 100644 src/modules/map/sagas.js create mode 100644 src/modules/pages/actions.js create mode 100644 src/modules/pages/constants.js create mode 100644 src/modules/pages/index.js create mode 100644 src/modules/pages/initial-state.js create mode 100644 src/modules/pages/reducers.js create mode 100644 src/modules/pages/sagas.js create mode 100644 src/modules/widgets/actions.js create mode 100644 src/modules/widgets/index.js create mode 100644 src/modules/widgets/initial-state.js create mode 100644 src/modules/widgets/reducers.js create mode 100644 src/modules/widgets/sagas.js create mode 100644 src/modules/widgets/selectors.js create mode 100644 src/pages/app/bg-fixed.svg create mode 100644 src/pages/app/bg-shape.svg create mode 100644 src/pages/app/index.js create mode 100644 src/pages/app/style.module.scss create mode 100644 src/pages/not-found/index.js delete mode 100644 src/serviceWorker.js create mode 100644 src/services/api-service.js create mode 100644 src/services/dataset-service.js create mode 100644 src/services/style-service.js create mode 100644 src/styles/_vars.scss create mode 100644 src/styles/lib/_react-datepicker.scss create mode 100644 src/styles/main.scss create mode 100644 src/utils/jsonParsers.js create mode 100644 src/utils/nice-date.js create mode 100644 src/utils/query-state/constants.js create mode 100644 src/utils/query-state/index.js create mode 100644 src/utils/query-state/stateToUrl.js create mode 100644 src/utils/responsive.js create mode 100644 src/utils/storybookProvider.js diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..9da4837d0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_style = tab +indent_size = 4 +trim_trailing_whitespace = false diff --git a/.env.sample b/.env.sample new file mode 100644 index 000000000..7e1e7ebe5 --- /dev/null +++ b/.env.sample @@ -0,0 +1,4 @@ +REACT_APP_MAPBOX_ACCOUNT=mapbox-account +REACT_APP_MAPBOX_ACCESS_TOKEN=token +REACT_APP_API_URL="https://mangrove-atlas-api" +REACT_APP_TRANSIFEX_API_KEY= \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..82fcf5245 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,24 @@ +{ + "parser": "babel-eslint", + "env": { + "browser": true, + "node": true, + "es6": true + }, + "globals": { + "Transifex": true + }, + "rules": { + "import/no-extraneous-dependencies": [ + "error", + { + " devDependencies": [ + ".storybook/**", + "stories/**" + ] + } + ], + "import/no-unresolved": "off" + }, + "extends": "vizzuality" +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..348076b95 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +10.15.3 diff --git a/.storybook/addons.js b/.storybook/addons.js new file mode 100644 index 000000000..fb6433dfd --- /dev/null +++ b/.storybook/addons.js @@ -0,0 +1 @@ +// import 'storybook-addon-styled-component-theme/dist/src/register'; diff --git a/.storybook/config.js b/.storybook/config.js new file mode 100644 index 000000000..7346529ac --- /dev/null +++ b/.storybook/config.js @@ -0,0 +1,15 @@ +import { configure } from "@storybook/react"; +import '@storybook/addon-knobs/register'; +import '@storybook/addon-cssresources/register'; + +import '../src/styles/main.scss'; + +function requireAll(requireContext) { + return requireContext.keys().map(requireContext) +} + +function loadStories() { + requireAll(require.context("../src/components", true, /.stories\.jsx?$/)); +} + +configure(loadStories, module) diff --git a/.storybook/contexts.js b/.storybook/contexts.js new file mode 100644 index 000000000..d981ef8b8 --- /dev/null +++ b/.storybook/contexts.js @@ -0,0 +1,22 @@ +import { background } from "@storybook/theming"; + +export const ReactContextProvider = [ + { + icon: 'box', // a icon displayed in the Storybook toolbar to control contextual props + title: 'Themes', // an unique name of a contextual environment + components: [ // an array of components that is going to be injected to wrap stories + /* Styled-components ThemeProvider, */ + /* Material-ui ThemeProvider, */ + ], + params: [ // an array of params contains a set of predefined `props` for `components` + { name: 'Light Theme', props: { theme : {background: 'blue' }} }, + { name: 'Dark Theme', props: { theme : {background: 'red' } }, default: true }, + ], + options: { + deep: true, // pass the `props` deeply into all wrapping components + disable: false, // disable this contextual environment completely + cancelable: false, // allow this contextual environment to be opt-out optionally in toolbar + }, + }, + /* ... */ // multiple contexts setups are supported +]; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 000000000..327cf30c6 --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,3 @@ + + + diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..0caa4a6f3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +# Changelog + +## 2019-08-09 + +### Changed + +- Header layour changed. +- Header is fixed when user scrolls. + +## 2019-08-02 + +### Added + +- Countries list showed in alphabetical order. +- Hotspots included in Search countries modal. +- Header fixed on sidebar scroll. +- Language-selector. +- Storybook stories: + - Map. + - Header. + - Modal. + - Location modal. + +### Changed + +- Dates selector (arrow styles and functionality restricted). +- Global Styles adjusments. +- widget buttons styles. + +## 2019-07-26 + +### Added + +- API. +- Loader spinner. +- Storybook stories: + - Chart. + - Button group. + - Spinner. + +### Changed + +- Widgets load. +- Widget download data link to component. +- Collapse widget button styles. +- Action Bar in header updated. +- Search modal interaction improved. + +### Fixed + +- Sentece in widget updated. diff --git a/Procfile b/Procfile new file mode 100644 index 000000000..81e952f73 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: node server diff --git a/README.md b/README.md index f1028161b..2180e2af9 100644 --- a/README.md +++ b/README.md @@ -1 +1,45 @@ -# mangrove-atlas \ No newline at end of file +# Mangrove Atlas + + +## Installation + +Requirementes: + +* NodeJs v10.5.3 +* Yarn + +This app was created using [https://github.com/facebook/create-react-app](create-react-app). + +Before start you have to create an env file called `.env.local` copying the content inside `.env.sample`. +If you need more information about env variables you can follow [https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables](this instructions). + +Install node dependencies: + +``` +yarn install +``` + +Init server for development: + +``` +yarn start +``` + +It will open automatically the browser [http://localhost:3000](http://localhost:3000). + +# Deploy to staging + +Put all your code in `develop` branch. + +Add heroku site: + +``` +heroku git:remote -a mangroves-atlas +``` + +And deploy: + +``` +git push heroku develop:master +``` + diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 000000000..738e8a465 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,5 @@ +{ + "compilerOptions": { + "baseUrl": "./src" + } +} diff --git a/package.json b/package.json index 8c8f1529a..02826786f 100644 --- a/package.json +++ b/package.json @@ -3,18 +3,70 @@ "version": "0.1.0", "private": true, "dependencies": { + "@fortawesome/fontawesome-svg-core": "^1.2.19", + "@fortawesome/free-solid-svg-icons": "^5.9.0", + "@fortawesome/react-fontawesome": "^0.1.4", + "@turf/bbox": "^6.0.1", + "axios": "^0.19.0", + "babel-loader": "8.0.5", + "classnames": "^2.2.6", + "d3-ease": "^1.0.5", + "d3-format": "^1.3.2", + "deck.gl": "^7.1.4", + "express": "^4.17.1", + "express-favicon": "^2.0.1", + "lodash": "^4.17.11", + "moment": "^2.24.0", + "node-sass": "^4.12.0", + "prop-types": "^15.7.2", + "query-string": "^6.5.0", "react": "^16.8.6", + "react-bootstrap": "^1.0.0-beta.9", + "react-csv": "^1.1.1", + "react-datepicker": "^2.8.0", "react-dom": "^16.8.6", - "react-scripts": "3.0.1" + "react-map-gl": "^5.0.3", + "react-modal": "^3.9.1", + "react-on-scroll": "^0.2.2", + "react-redux": "^7.0.3", + "react-responsive": "^7.0.0", + "react-scripts": "3.0.1", + "react-select": "^3.0.4", + "react-sticky-box": "^0.8.0", + "recharts": "^1.6.2", + "redux": "^4.0.1", + "redux-devtools-extension": "^2.13.8", + "redux-first-router": "^2.1.1", + "redux-first-router-link": "^2.1.1", + "redux-saga": "^1.0.2", + "reselect": "^4.0.0", + "viewport-mercator-project": "^6.1.1", + "vizzuality-redux-tools": "^4.0.2" + }, + "devDependencies": { + "@babel/core": "^7.5.5", + "@storybook/addon-actions": "^5.0.11", + "@storybook/addon-centered": "^5.1.9", + "@storybook/addon-cssresources": "^5.1.9", + "@storybook/addon-knobs": "^5.1.9", + "@storybook/addon-notes": "^5.0.11", + "@storybook/addons": "^5.0.11", + "@storybook/react": "^5.1.9", + "eslint": "5.16.0", + "eslint-config-airbnb": "17.1.0", + "eslint-config-vizzuality": "^1.3.0", + "eslint-plugin-import": "^2.17.2", + "eslint-plugin-jsx-a11y": "6.1.1", + "eslint-plugin-react": "7.11.0", + "typescript": "^3.5.3" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", + "lint": "eslint ./src", "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": "react-app" + "eject": "react-scripts eject", + "storybook": "start-storybook" }, "browserslist": { "production": [ @@ -27,5 +79,8 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "engines": { + "node": "10.x" } -} +} diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..ae57893aa419da2fb134116e07d87e806d672e9c GIT binary patch literal 8121 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 literal 0 HcmV?d00001 diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..fa1c7974dcc2047d10317753f89bd27675497ac2 GIT binary patch literal 8537 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 literal 0 HcmV?d00001 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..40b7740dadbc327f10630d07b8580c669d189e80 GIT binary patch literal 2163 zcmZ`)S5T9Q8vSWOIv4>7L4pX2I!ZuF2;fQwX;Kut=+Z$%LNTE!E3ttB3(}+D!Xko- zR0Si*(hW%nMWh9g76YNT1X7os`*a`f!*|Y{Z|2N5a~{t3j3h)7000>Wdu#Ut z{_yV{5;;IgF!KNb(NGI#3jnCgl;Zk}9cWmfy}L62Bx(ad@M>bCuw`rYkU~g^LNwwO8N5Q ziN2oai!>EdXC8e)k=P;9(|WkKLzUFE!{bh{b_1v*>g4WI_ek>xAqoXJ+F1jsVrupW zLLBYjY$HAoIwB>jYlx5-0syg92Wtz@_=(lxFt53b@{c8i!1iqhp!BEusT`x5&1L$G zPnq*;wG7LUSn7vzf$F07tsAw@${7{LEVRi z2WUac2Z z+hFSwX?!#1)7Os)Z2~M6<;ub5*o-QOqKMIHy+((_%YSn;=!u+**v+3m1Ijx!W z8XDTNl{#L5BsT^H`8E#uE%WL-xzLs0F_g2xc`yz!|A^k{I$MueH?jKsf`Y~AoPvVL z6(OsPVqfcYpP*xc=@u6n8n&#FNyJoDO zQCPW^+f}hEbr(mQ`qD4l<;rep@==02+=|3@QtdNy7b**8QF6K-B zhL|6uHm8zvr>E-GXF;g6u-K!mNttA)C068$?m0Ds0dlV-KChj7A&DgItnF8c!!oPO z?#z#Cj8^4?;ltz$re1E;nKV?jH%`~P@^<=#J9JDKId`OArA`|U!-l+)b;oBBoR(}J zDU`q@UUjk8`#V?<)|a-@ci)2wWq0(Yla9YrFv7%j)M9Gj+khDZmQs<8J85qIFfc+w zR|oz$ZecM6y{K-NPM45l&V($kimPvLV_M{zG`zC#a!hZ0X)i zDJ&l^_4Um!Z%rGSxp0rqO^So+NJKHl<-T)EH>OtR@r2Uz_O|PL&mnf4n^hKaip#|I zu>2;zqsmSV298a2YBq?{B2zE*I4yw=6)PUSx0uyT$R-a?{dlWuQH>ULlKIcXo6QlM zvKtK`kYn3$!hjk{5_j@Ch^J9YkbVE?8RMz@V&178#TVSzF&TVd$mNqe4C#eT6v|PQ zT6#`3*y2e|@zssq$^1g$=6sWKaxTR}PJ6QawZV~IDJ2q;>zvJqr zWN3t5(dEgg5mM(>92{E@3K9jUEFVI5Uc%y^-(ZR^TnBW5f^x=9YXW&L>^x>X* zdPqG3q=E5ixIPjN=QB(q{>Kmz859HNlQKM?}Cd>tut@AQ8<5PZ()z mn9xP;f_90MlI=?_o&^}n3g|6M!@PqNfP;;Tb+x76{eJ*ky+w2Y literal 0 HcmV?d00001 diff --git a/public/browserconfig.xml b/public/browserconfig.xml new file mode 100644 index 000000000..d416bc536 --- /dev/null +++ b/public/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #ffffff + + + diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..815384b334af4b95a8de2b57dce36e849be4bdaf GIT binary patch literal 596 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstU$g(vPY0F z14ES>14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>`GFkBz`(>QAj~czDZ!;-%b`|# z?$z=?XJ0ONMCJ(l=?0GlUV03##05(}IhjrcTEXDy>gTe~DWM4fGL+Dy literal 0 HcmV?d00001 diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..709a7f301a8f365c5bf07d46304cb1068d64eaf3 GIT binary patch literal 896 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 U(@p127P@q-UYaKR4o|wwf0f0@bN~PV literal 3870 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 baj7?x%VD!5Cxk2YbYLNVeiXvvpMCWYo=by@ diff --git a/public/index.html b/public/index.html index dd1ccfd4c..fbbc38adb 100644 --- a/public/index.html +++ b/public/index.html @@ -19,7 +19,22 @@ work correctly both with client-side routing and a non-root public URL. Learn how to configure a non-root public URL by running `npm run build`. --> - React App + Mangrove Atlas + + + + + + + diff --git a/public/mstile-150x150.png b/public/mstile-150x150.png new file mode 100644 index 0000000000000000000000000000000000000000..976d88ea5a204d63cbc1ff822a3221d6c58f763c GIT binary patch literal 2596 zcmZ`*2{=@38$M&1u})bBsVrq8G^C8RG`6v3D56o!AY05B4Tbc#n6l-wmCzSOWJyvO z!!Ls@Lp6Sip^&X*WGh>IXY}=b|NmWo=X%fczR&&K_j8{2IoEa0d-bq`mAHt42mk=$ zSZi|}06^fpTZkV-97BXN!AZc+%-#$DDw0K4y#zsDiC~Sh2Y|C`01$f#0G2^0b`k)> zQ2_AaBmfxR002a2cCC{!n5giuor?uX0NNP9GJK*CVQE1L zK0!%{FrPTTgrF2u1}cl#j?^(ch|zP2%&zX@jD4KuQ?)+-WN2Z{;NPDWEpNR0E_f(s ziceCTHoC>VO;A07YrN|C>Lnju)O(!Yv1-%vk;%-r4KbS`QUW4kkj-&?f;BgnbiRXS zLA-6P%oBn{UtvH8M#0)!z(%3cqHt6-N&pD}&{(XwnM*|f=fNoq+gc79Iu>AX>+Zoz zWR188r6Dq=MzBUezzkw$MwF@hGS0#GuCfFB7ObPg9^d?G|79sUu4_eaFE*>Kvw{HT z|J8N!Q-n=```Ihq)7_o(E79^({zdAoH3Ll5-9||LJxXmhUy#={1!u`&k1DG@GLJ6_ zUWOb&T+)`_ci8QE_h1Xrz$pzWZ#iQksa`%QK(&=BN4uyTvzSSAk<=JJ9dal=Bi&;g zqLy&Ct|X1l5>7qgByTHwBB#?NEB|>>G5Q(peu-i8;F;m#z<^Ov)x0MKS@iGTIc4$w zor^wVMMf6`Fdd@#X!Q zz4wOssQVo7bvrZ?Xts!&kEHQgnG}D4i>}&XuJ4)u3G`+QLCf)DW$H0~Av6b7xjIq~ zT|@ug@St&7za7Sjc49V5MPqk^!5zk}MLHY@_)(5+-B2K5Zs!>aOJr#@tQ}sveMs2rkfquU?yOUa# zp&@4Z(v`H3w!YF)+Zf^Ah%ZgYEx)_om)ml?C3A+7Xlkl4xE9rruucD{rx6yz_AGxf zY9&u)j9=?$JG!T|zh3$tt3*3xy_z#P=XL0P=6&r}ik3}eogI9;4UO*Y>_NPR7(&}) z*h8Avu^}YapmBjnLb+y=K{6pIB>_X8i>g(bEzw=+jRbdjCc~I6P3j9$Ne=k(KAWbe z7kv7+<>^SyzAYVv%hwDS9PAXQ3+}&kAy3=Y@;y4&fZ**82`k{^7K_W^R9hEePNXQg zLd)bs+~P!{;Qe|@*--bT08P7vj98C}IX|h=BhUlaY%pw7xFo}gHC{FVgky{0_Oy6{ zH%)|7iYVyWH2DEZ@;}v&EPh(OfLsX9E47D*_$+wpOKh)T;q`<4GSTIf1X=>&?nU&| zPux&f2ODMWX9`AE7A-pcZx%oLS*vr>Q@_$e97N*nFzjdM?`OAB$L{KlF!Lx^6Z7Mj z6ZE+4*r^`tey#mUrvo(YC(5!PJ4TUMCf=iy{%Y{#XfO;jSJYyFez5Wg`dwM}d!%Je zuW1EozDjbm>NcG`Kzh6opP1jk+DqE}^jpA6>}xc%|7r(?IyB-(Zcb^#8wlq#>n6R8 z*tL^8of?)GKCv|hDq)4wZqlvZjT~0N!=8p379H1LECNgYGMC(S+wK4ydc5{V&xszl zti{_&gUc@rGfxFdZqs~E?h96mo2YPxQwO164&z|SB@3uKW-vCMf|9q{`VA(_zHEqmwk<*if1DjR)zCB-h2{ZtcJh(yytuhbtz?-U?Nct ztz8&ei(4huqd$!7eic$YS%4g=8qwgo2czwVY$xJ4RsCLdI~pILTiAu=b&MSl*%BJr z(4gaHgzpkT^Gtr&TOx_zWUHJr_X77~eMih4O-C((M4z7dhtG5287F%x83!jOwu@3? zOdp9-yq9`Uxp7=K*&aZeZkjH*;4_-<Zm}Gfxj*U1K@4d%7>LU3h!KZSvdpPlbh_?|lk= zD*UU@|1a@(*2{mY<)WiwsH117 zhu)3SHAJC=^043$>c0fRAw+-Q$X^R0*Jse6!1b()8wKY@RSG4C`1%L=C{ZFqeU$t| zDd68LB71aJOo()J*RFQQj!x_t4&YZ6)h38asY= + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 000000000..b20abb7cb --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "", + "short_name": "", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#ffffff", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/server.js b/server.js new file mode 100644 index 000000000..7a3d6f5e6 --- /dev/null +++ b/server.js @@ -0,0 +1,11 @@ +const express = require('express'); +const favicon = require('express-favicon'); +const path = require('path'); + +const port = process.env.PORT || 8080; +const app = express(); + +app.use(favicon(path.join(__dirname, '/build/favicon.ico'))); +app.use(express.static(path.join(__dirname, 'build'))); +app.get('/*', (req, res) => res.sendFile(path.join(__dirname, 'build', 'index.html'))); +app.listen(port); diff --git a/src/App.css b/src/App.css deleted file mode 100644 index b41d297ca..000000000 --- a/src/App.css +++ /dev/null @@ -1,33 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - animation: App-logo-spin infinite 20s linear; - height: 40vmin; - pointer-events: none; -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/App.js b/src/App.js deleted file mode 100644 index ce9cbd294..000000000 --- a/src/App.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import logo from './logo.svg'; -import './App.css'; - -function App() { - return ( - - ); -} - -export default App; diff --git a/src/App.test.js b/src/App.test.js deleted file mode 100644 index a754b201b..000000000 --- a/src/App.test.js +++ /dev/null @@ -1,9 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import App from './App'; - -it('renders without crashing', () => { - const div = document.createElement('div'); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); -}); diff --git a/src/components/basemap-selector/component.js b/src/components/basemap-selector/component.js new file mode 100644 index 000000000..153ec3366 --- /dev/null +++ b/src/components/basemap-selector/component.js @@ -0,0 +1,71 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import { basemaps } from './constants'; +import lightThumb from './thumbs/btn-light@2x.png'; +import darkThumb from './thumbs/btn-dark@2x.png'; +import satelliteThumb from './thumbs/btn-satellite@2x.png'; +import styles from './style.module.scss'; + +const thumbs = { + light: lightThumb, + dark: darkThumb, + satellite: satelliteThumb +}; + +class BasemapSelector extends PureComponent { + static propTypes = { + basemapName: PropTypes.string, + setBasemap: PropTypes.func, + isCollapsed: PropTypes.bool.isRequired, + mapView: PropTypes.bool.isRequired + }; + + static defaultProps = { + basemapName: 'light', + setBasemap: () => null + }; + + onChangeBasemap = (e) => { + const { setBasemap } = this.props; + const selectedBasemap = e.currentTarget.dataset.basemap; + + setBasemap(selectedBasemap); + } + + render() { + const { basemapName, isCollapsed, mapView } = this.props; + const currentBasemap = basemaps.find(b => b.id === basemapName); + + return ( +
+
+

Map style

+
{currentBasemap.name}
+
+
+ {basemaps.map(b => ( + + ))} +
+
+ ); + } +} + +export default BasemapSelector; diff --git a/src/components/basemap-selector/constants.js b/src/components/basemap-selector/constants.js new file mode 100644 index 000000000..86a099d0c --- /dev/null +++ b/src/components/basemap-selector/constants.js @@ -0,0 +1,16 @@ +export const basemaps = [ + { + id: 'light', + name: 'Light', + }, + { + id: 'dark', + name: 'Dark' + }, + { + id: 'satellite', + name: 'Satellite' + } +]; + +export default { basemaps }; diff --git a/src/components/basemap-selector/index.js b/src/components/basemap-selector/index.js new file mode 100644 index 000000000..f23122850 --- /dev/null +++ b/src/components/basemap-selector/index.js @@ -0,0 +1,16 @@ +import { connect } from 'react-redux'; +import { setBasemap } from 'modules/map/actions'; + +import Component from './component'; + +const mapStateToProps = state => ({ + basemapName: state.map.basemap, + isCollapsed: state.layers.isCollapsed, + mapView: state.app.mobile.mapView +}); + +const mapDispatchToProps = { + setBasemap +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/basemap-selector/stories.jsx b/src/components/basemap-selector/stories.jsx new file mode 100644 index 000000000..75394b7af --- /dev/null +++ b/src/components/basemap-selector/stories.jsx @@ -0,0 +1,14 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import Component from './component'; + +storiesOf('Basemap Selector', module) + .add('Light active', () => ( + + )) + .add('Dark active', () => ( + + )) + .add('Satllite active', () => ( + + )); diff --git a/src/components/basemap-selector/style.module.scss b/src/components/basemap-selector/style.module.scss new file mode 100644 index 000000000..23006a11d --- /dev/null +++ b/src/components/basemap-selector/style.module.scss @@ -0,0 +1,60 @@ +@import 'styles/vars'; + +.basemap { + display: flex; + align-items: center; + + padding: 15px 20px; + + background: white; + border-radius: 10px; + box-shadow: 0 4px 12px 0 rgba(168,168,168,0.25); + + &.collapse { + display: none; + } + .current { + margin-right: 30px; + + h3 { + margin: 0; + + @include upper-text; + } + + > div { + margin: 5px 0 0 0; + } + } + + .options { + display: flex; + align-items: center; + } + + .basemapThumb { + margin: 0 0 0 10px; + padding: 0; + width: 35px; + height: 45px; + + background-color: transparent; + background-position: center; + background-size: cover; + border: 0; + border-radius: 28px; + + overflow: hidden; + + transition: all 0.5 ease; + + &:hover { + box-shadow: 0 2px 5px 0 rgba(7, 127, 172, 0.43); + } + + &.selected { + border: 2px solid $primary; + box-shadow: 0 2px 5px 0 rgba(7, 127, 172, 0.43); + } + } +} diff --git a/src/components/basemap-selector/thumbs/btn-dark@2x.png b/src/components/basemap-selector/thumbs/btn-dark@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..6626d2c9901a59b308278dc47502ed7beedc0ebf GIT binary patch literal 7979 zcmV+`AJpK9P)PbvPXUhx1z0Dc@##)*$;m?q)%`ZjCnY8>5zJ&F|n`glXVGtvKRZEsr)c$a+sbguM^+21`84)*TJ&-%HC0*3v5s>)vKSN(ALwBO_X zhtH`PChL`{W)W4tOa~UQ*tV?W|AvsI!0BK>=WTlTbC4ojs1CPck*x0{Dki9uKTcSqjB&q8dnQ;cm$=Us zq;#ZvW!iPy)W?O217PKQe?0#=51mDWw7Ukk-lSnYWPLzcgJQ59YmKNBLj98~^I8uF z_voRCvIn685#cDqx4SMaR?D<$nzREIz4#tQk>a7Pqc5ux-h-gRh;SM>5Bo{~8jee5 zzr$$CA#(dA#zGriM=;nU%Mf8YzTJ2G)Uvvhz^?4gT0M^fbdMg_mt0jbx=PR`IuxPx zcg-d(nMAd92;6n;QE}V>+aLH`_L%(uTp7#pTdY&QnaEi`-Jxt6X~5cNi#h0JZiMQ1 z4dQJNu|8$3R_g_^e2_+in#JPxAnIN7sf0f6p&FX)F70rQ)q0E2IStwpWlFB;l@*ii zxi;5GWmR*B2!U^^ig%y{MYury+1FBz`3y&iY(_?-&0;wZG`oOM2(J=uxKSq&SE;DK z_!{H!!mFRs&$yrV-f4=vpIvOFwCbo1fuLE=k<|ii)+u9i^x+%aDkj21t{{ZoG79_ zTgkvZKA70-aEZI-J_K>F>Dfi<*ENd=2@B!24`l^UfSMY`d7o}%9-y#JE;3B!`u$<* z?LipsRe_A^(2w`~9*{+;nopIP2|(K4LOu<;_vAY6cs=F0;O=? z2&H`007mv0k0xPN|7dBg+$aAi5PHvNuRk0>2d*LtM3io^hF5}oxj<%j$`YjrtghOH zGPRD^Y=euiMp>h{L;B*`IVdETAZ-5q)tYPmvEW$Ld;H1s%OgFwW0&q{>!hoPtFR6^ z#()th=Z`w_w~zY_hogv?4Kh_1pkldNq-L{@WfWX+iZJc2g;Ta^2r>qPA&Ai_e<&+5E=l9UUTemP| zD1QLD53FlW-ef3tZGj5p65W0lp)nzAEn1(a8?&K0Ju?$Hq*2&sS5Oz8wQ+cir`4@b*+N=V_t|Mw6)e@H@7c#^ek=J>phBhK6@}W4n zZ_jtZF$HgeFcba{Z#agC*b~bgv0tli6k!g+9VG${Ye<;^$0*QA86vI8Tf7nP3nXvt}rX7je zVnrcH#MGFk6usavqb5ejfDr=VI}zNk2CQ3`GKWDj46Z_gfnyD<*QBc;kqL6tid-N| zS6F4cZ5Y_^$XG#6jXOo;8c%~n<9pB|8j4_jibL+uial0ck~B4pA)(1r!9gyFYxANq zDT^ZK_1?FMk&khRloq7^+g~SX1V5$SEKH?=3eL|VRx|2wV}w1qicu$$OXM<|=J`dn zITlLf$M02M$c-#gLddM5B%2+=xmYzRu?qV>ziTQ`$=Dpl!9|!z5CI!3VRpL*J)kmj zl^xeI1oscao!0ABAgDyS1>6(TGe%elr8A`vOBd72v}(3deV>%$5`}lZw@WWywP`&g zo8~$rrPg0?LHDe>_TR1%LA9;`z>P>7;ix2+)`m{xIzOtCleRX z{`{}Fnju@VBxk{$!q>QPa&{id=m@tp)-jviw$weKN8wvJhQpAa1s0lGLfErV-HFvJ zy!D6z=Pc96##M|_kt%FH6X#y?FIHy|}#(6dzoV zKFfF;Dt0ZbQMV$OF+EJu6i}J8nx9QCbZzso9dmkhaVHHbqbAtQXJc_Wf?i0CzNu8Lpa4&(L0zt}ZWwywCt)G0XOE3JPTVI)fi&hFjm9jH zW)&_Fbr?j=$*M_l7=D3AX_HVvzbNf&Zpb#wD1U}bx6II8ivDsq>4&`-13g2A>Z z0pDI9qYeU0rsLEHNiO*6>IwyrL^XzY2IZXYqHCU#MoHRe=6U%^c&mK#pg=13$Z-ph zAiSSkKMjPwc=a;PDfaY_^D_*%ou}K|+q8+=jL1XoAs7_sK-!R98%Y^s?{^{w*XpoH zd(5!{y_jooRF?X&1);*M16`ZH)!pr@Si~I5dqS%XK}tiBxF+#iu2vvn6qA_s3ZdKA z4Qr+6VwB?Al4A17%^3uSk0fHEH#M6bLYn$ zHO7jh(`aad;`RROs!QBSUa&rkq7--8J0?F`-h{M)Ty<$Rhx*|dP>jy3?@EkbSpXBh z4y2C1MI)g+@!zc^2C$pqzvlvwf%Zt%uk}hz?6$;2Yf!Q{ zR=-8MI7g`<=KlUZJ-xX}XCTMq?B(5U8nVu?GNd4%Ilt+@+@W!z2GdoJ?qWV>gyRAt z;0jX^u9?4uI|WxwPILj$OC6O^j;(|70R)V(#sau=4&lrVn!Qn~gK$~MU__WSQ8T^I z7Grc=;2vdBAv5?r%G&N;W7z@YO32%AHIq2q((rEPdq%{YPN!*u3zxW}ymF7QBD4bm zJ78^Dk==NM;1se%BYQX3xNnOjQLB+om|k36U!$;I^p#IF*KCf0;Y?Eqm9^^{+zL)( zUD;*q519<+j5u;ROQRO)wz;I2+-!mmP<#^^FD{9znv$DONy1xrs6mi%Jpu*U zDCR1zLl^$$>#yOQNqY7AF1XBmOQd}D<+Jqd*OTBfk!azlF&4P>-TiI4ySooJQCWZ{ zpcJ_oG8@%QN2HXBdCIT8|I>3#Eg8z(SC?1#HBkf0=?)U7uovd%Sa~!a1MiD8nVhFF zE-?(Z%HeN^l8kC5eEO@oOUDW#cVTRy4XWWZ{GCPXX8N{c3S$xH6&rZSj9ISOfvzZC zLAJTbj`Obd=ISQ0ZBe9YnsIx&(mJobFTte><0rS{xeDU&U zg2NI;jG|>?YM`;@uu!;pm3g7UofIPk7;r*>P*S2+n`Zc-jIB>cIOT#G0#(2P0vmSQ zZO6d)AkdWXqIF3q?8`+UP9bTQMwF?HX!V%kEj5e~-I27>Cv6PLPlZK4g!e5ZZ^-5? z;}U97)S=`9DZr4lK*ThgWstISiNGD!USP~ucdtN9!#b(&2)FGqN||W>{l(7_b*IpP zMm4Hx2;XbyUO7sHgK&41CnDv~;>#cZdJu{-xr-I*aRDkZE6$O@o(fQuXd6PCTIPDv zl&~hIWk7|^i5+K|*o4lyiI6DEqo{n2tO7-$lTe^hC;B8H&~s5kX?c{aLRG-2jx#T@ zWg5L3+*2+wT8-g5v+Ol>Q;lnPxQ**6`?{U(ozjG?e0oXfNBvVuiyC4djt23)F7+B> zUX!+nnAj4Md_{^@@YhW!7i5b;5*5~xp}*=wXqy1V%~(B?-G<7AF?3^ZpmIxC_Ql2qXXHlJwJ1uspo<)9SvA9l zUQ;&MYK83CP6_1$kOsM2W!d%m&ut~xAvfjEPocz*p{q2-5QWGp0) zNOUf4hwr%s%cp1$h=c}3x1cRw6te@BZdm_c2nMV}K|yLc=Y^h&U#))~VEhVlF$kXFf|xraZG z#{&d+f1B1JRyS0O0?{K}V^@+fRhU8m6Go?n_x}iq!nD$ITx@_imtneQfpcb6Oy|EWl3Hx-h#|m7p;p#j~f#m;1e#7MpnVA z2y>@WK2eyC#K%ln*RZwec@?kQ_@2xplxTrz)o(!4qHYHM4NJ;Db^vW&nF5r%GQHZr zWKP|vNGm#YlB`i?Hp{MOOQf#d4RDS_RNKy9G{>5Z^|@cY!|g1Fzj%TNFbGOUv+anG zNAbs*Es%?eN#w}Fiqk=bWm!Axu7K$ohvB*haOsCJ)6g%$I;Tx}=jFdZuL^QWx|i)!+a@FWOQOU_UG^x$Eg-XGbFKMurcPoVD`0-b zT11V9WB2Tz7_!v#gvAKyt-oBvn9db9%LfIJ(a)wiy)3{h#i@`5bYH8{Tah!XP}6>DcPDs2Kl&bLrK_Xh-+ zO5rq!jeaB7c${k`cP-sxpA6ZJV2v7b42gq>h)&93o1!ul&o=iVknZAzrsu*iI(sgR z#VD55w@{Qj{LiYK7tWz1VZ7%N5YD-m4en5r$)918F;SQTx!4mL=F5A&qoZctgyM}r zzRn?fg>{h;-Izng+0YiXTbf>&hjp+{?$7+e=U5xxp-qW6dbwP$I}XNX@42DnIDGrjdYh;e;I5i`;NQ6+Lcbfb*WEV$)^ZtKI1jwMJ$vmF9Ec|Oi@ z;xJPT1r{+(A2P*L8=^@iaIX!mHDv8$XEx%@s5?+z*<7~yEK;<9!uj-(d&Rl%Q>)6K z-J>A>E3BleQd}q(=EWR^34L*snRJ8pbt^TMN8noE96}+G))pns9^?zE6kKI$5byIE zi^TkRTE5=0gNZ@LF>k`4qIOIB!tflM{lhN!iwjc&kGMsv9Ij(%bqz5a_Sb}Tb1LU` z_jLye7nk4tiCPYLf{2(1i20a>5BcofUf`_TBR#a<1D@aqO)F?V0@5tn$s}zH(I7&; zR)5}){zAEa@Uw^ch9`Z|O|)|Kjy>lbTp2Y}e9qT8kZ2Y`CV{%T6}6>@jd$g+Zd1S# zIV5+HY;VU%8@rH<9?2tLSesdnn5>#d83Zob4T}xQ>afCUiL$edzr2Nc)RHeA>N?yO&QN*bUU30P0LBU{;YPlL61~~I zB9&R|DqLd;VDf2G-Ro^@dQG4D$no5XvV80n&)&4v_-?a{&pu35j<26S zSChguk|}$vOq^Xu9FWMN6tmhlQYgu<9*RjAzVx9X9)33eQDDw$xj0U*$B%EujO#=T zioh72*9DD&9=tmSfw++KD$@ssh?e~L4>!)K%uP~#4ky|i+~Y2Y{1{m=*P7Lv)S4#+ z4KPqB+u*_XqTNP(W!K$3VTsDeP~{l8K@LzE5?4H%`>sGI!QOclaq}4#Z8LwE#(TU0 zPX2h*mM<#8EBJ!f*viGaK5k-VFNEa|m1P9-I*U!CT<3S(X8B^dVISE3j!Mv73@9qa z?tu}}Lw73*_OiIwD%<(3-IgSmx!ZR{y^n{N4B(IdDntkhKL^%NIS~tvQUA$5x`aY{ zxbWZ#->W44i{tqA4I;Ql1bBm+{Psz__jE(vs?EdNt8o{?c4^*qAk~KkM7@=>-2pf9 zP+1VRB<@y<2H4Q57!67ONFp_t38sbR#pN}C=xA}eLqcZk#`|)`#yNx7riVY%Tk2GQ=!lJ5R4LoP!uXi%ipoA|6T0T>;COOZLuR@+?mA{lEPCVMfD< zX}Hk=-cuWI-R2NDW%M=7f)&&s5uvhFkck?B_xKIa;Jn{OVtJ~xQH~`*>~UeJ$Z1f9 z78Xpg4~CKy+$2;NQt|SK{S{zEFDO4PrYlWIAl>b-mC}eTt`o&3dnE*|qR*!JVJX@U ze?*lOb{~L{w{()F9Hxdz{5Esc0xA8r~q8x6C0?&IS zS^3ZJ95c3xIWJ*~EJLl=+jHUT7&~@3I631%Cm%ZSkffywxz%lHTMTlGWzm*3ncYXo zpVFv`rghx*;eSMn&)5`b-~2$KEmB>r`9aiLk}20kcFSVy)yfrSrI}b&k^Lc_*YRGj z<+|AOLMuiN-OB52e>i3!3y$IHD&!IHAuuY%J*?smB1-pA@l){pqbpE5L+m1 z{Iemul346x9ARw+IzyP61-W>;edZFHSlFL-AxsOL*FKDUM7gSE1eL@hZ51G1G|Jqa zjJ$+}N9z-ph%7zk;2(Q{%7&)4yk-riBJ3%vSet9MDZNiuwkm(Zqibj6DL|DOGD62B zd2)+UP%$aoEb~&=Y1tCkKhurtyOao;V+r`c&krJv?Q0D=A7q24&lh15R<C zX!c~bthF}j!`-aNZI;Vfu$GWkGp&R_v)G!r!Uyn{b0rD-=f8I`<4#7&TFXFsy+o_^%@WTusF;4 zXd`eBMMFUR6sfkPoNBna%G}|Qt(X=uR*rLoK-Ml$TUdMK=>;GWIMB!=9z)nK+WRCTe zWZCcpx2_Qn2I;pfm_W_MMkFlEcjikyi1NVqF7|*`a}*+c^0On68_=)m@tCdAXV7O8 zo@?S6JH~P6;r#px&e*5<-7MW5;Id=ycSP!MSXPU=+R5GS*oicO^|}%6uhOj6jcB$X zd|2^=6%+km1{1EP=td>8QW4>MT_gWHA;=cD5bie7P3u&e)v=wy2m!i?P+*ZF&BjyB z4nR)kDEQdssL4;<=vz=NpN;MIRq?c!F6nqS>~*)RYo8|7g`4QswS|3Nc2eOzk?1u3 ztCJPu*moYnJtN%SRw%amaK#`A1ce5fWl^!UpAP)3|R{yARD z`WYj4{Zkw{cwYXs_V-9^nxm$cn^G!u>n*noP7>#nGU4D)yMyCB)NXGx(;`{V_# z%BViR3ykuX?R&5Mf4uR-l;Y6#oll3|eN{vznhKsMjoJwWt)dKYBOin*A^*icL_?8Q z=RT`E!u>Rw{~+EDpuA?|&CtDM>)1nNy9Y1nj^U-K$>N*e#UDwUa9^iM$N_h;>NXFQ!ffD z;+({*KtAu{hMA#WrGOekaI_de;Azj?sE3|{pL`<1hZu7vo*zK)-rt{M-ZEn}vQS9! zU&vSpgCq+qU7yw4Zq!qRU^5kwR}{?`LpCWEmbMzZG?@$VWjS=|5r7dZnRhAB}J$LUSH zqPgP{S9mV24HZTXw5OSsKb@|8d=0Id^E@i%SHWU>?P)7TCpo5+Dgu;w6)&E#F7kPRdE;hy0SiGk-!- zaaCNE!;WmZtXncE-XutZxX-0bFAaF;IP82L9MhW)8rM^$=g0IekGc;JVrRD=tyVi$w##vLb|gk?eb1d__Y|*Xy8pP4}`A z83lbQ-uviI?A4d!ux}{-^}UFnUG0y5`014>>x5fZudI|^`uX)cYq7Abpf>AK$%zIA z24ZGrCdM_4CMPFjWMt&1+sf@guEb8bJYrG>I`igu>MxU(%O}`mmwC*W5FI(`{FfA< zvTD`oxPJL`G%BsQd2ccP_`%Ki;hS$LIF&f)*kf6i@%#7h$IZJtF*i|(Ms47DV3=nx zU}tA1Mn^|uXlUptTP(m%x^ysdM_hMlnqG%;8KyB0`rPl*Hc*;AG<5bfHwX~}edV}z z>2$RB_G47jz|X%u9EYV!e01+#4EEoQ>({O-aDAdJBq4(VyVGdL2M_k+`lXpTH#->p z1GOSB5Hy?3*xlVt9Lz1louK4~dQJf6d7@C!HMG}>-u970VssxRM-I@L*8LeyuGQ2ZapTcmoS*5BOLLW!IsFP4!v#1H4_Wm0 zZ;r-q`!2+Pd~iQ@+mB*mY%m@#u0^A@7e9XUVoZ$G;?0XA@tb$I;{Makc$uB$A(#)PusFn)NcrmM|<{O3o_ z`1v=h@ps==fYM*+%olnsGtEuX(XZ40i*Jp~rq|++x1UA1w5y{p>aB8YHXg^1u8zcm zN87Qsa~K=jb>-7oak(a28jVIwOiUa(WZ`zg4xZ#pj?3xHYgv@^n}&8T&XcTpO-t6K z0I4MW@7!soEc(fnsxqXTeU?XQC}bQeqe8*ZkGlH%O7VE58Gra>G4|Sx*lm>Jd)H3I zcQ22pp_T4q2nvG6#>S4FK?K_{>Q?GHrTabqxNXB|pTuVwrqAij>(Y8mi)RXN3Xo;@ z(zFMt}z>dk;J1w5Q)V*L!*0&gGldIA{RRFP2$gi(hyE^W>%L1!b4U?Yu-v ztx}GG{&G}xZ0r_tO_T1sF@(cP=|oB1Hcc-cn*6X}%Ye@YQ#T%*@x45G+w_#^F^_fT z{XS99SeN^AKG(VYPYm^?;5=DrXby6|Xee!MZ6$@=^;UJM22cthz{d0%V&RD}`!U{p zS=W(Va3|5d442C>e{PfYA;SSOoq3_@g<7>7>pKUUQ_AUiO1FP$?y7Ki3)E`0ROZSO zBKxu7pn8?qF!w=goa+{4IqvV}aho#dH6M6@tRM)za*Ez=@bfmbriXWVWi_@NwKy|f z)lDP!Zxjt>xTk)p{+gf)Tj?tk?j1Kl|xnd{oK3I%9i_NH5A!TC9 z&W8I?Lfvf?xo+cL)HdESY`9DCIY4ij;N3PZmuH;mayhQ&K{2mfjP2bPk&Sx06cNAJacf~GDua`8;k8r6eTYopBTwsqw+#!-UL!2vdXZ+}ip z9lOlqfLJ%YhYHf`XC|W2?u*w?b4A9tu9S4=+ieyG!HCd2AUTZ{L5BgoMkdATvmx+O zcia5nU&Kq_Ns!BMzh%2bCiVl?Wt)z03dCTw6gyhTY43G!>=KiH(&TGqkT^;k!N$97 zCn(*Q<6NhY1ITd+(eqcq zEA5!p<+@Kx5WSFw63TNSW~Q`2_Y(*MeDwQ(#v`5dIvZwJhnJ7^AY z1}0^LmD2XG%GY-Ce#?i}hEsxvm&0{vsHe6b@aoO77QXl6_ERm7Xo+)US8GVT9mv2k zGt`lc9LKG1NlTl12Rv&Xmtnf2s}cqPE?v*>S?IHJ(>aKC5I(OVgL8i#Sj%>4xt48N z;8ULn$A`-CPe0;yPAP6a-iu#<(9}@SCAnjF&YR11ow5t1IH!fFQ)7qmbgi8p5xRXv zt(+T<@cgiYoJr`&1RDSj!0>zp&}SWbXkEJQcA@_Gi2!H$ls>?duQOwn_~rFlOpcUd zx2_vanV?g36dY_Zn3ocrK#uDZ^OdvJs5kcXuDZ)-{-cbsqjaEvwug+JHB+w4a}MvN z0C>Pb10c(W*>D2c3+~J9$Z3#=-+BcUXC^BOQeXV$Pffi}X={11xCmIM`7G0Aj^`E2 zpw^!T%KLhaB7S#v%Ce2LP=w?Mbf&R`;K&{iGn{cIQ1@d3G{|y4G_=uWnaHAT{VsDG zj7zj~flT?w9}LC!E>&Y;bw7T8vzhpF9_oN`snKa$UY}ms4r-xlM_JP$H(8W%0)Uvq z&gX)Sw`0xd#2-S>O9H{-!lGu;Lpt>EF5orlp5vSvY_ z!NIU_w{ec3*&+C;`{~?t@bw-x9=yTw-DkYZ6a>AP|F3tS=>7C)%<6^NzH9(;9?Q&e z&{#foc&Eq4wRqKzhl^Wr^O0^M>A(R44%*3FD1yO*<6u4kdILe5erFZI;vg{eA;aL{ z)3#jKU0P?s*9{!@d~$yy?muhAkFSpEp|G$e_azSo@_WHL*-{7X*ziDn`_iB*d70xzw`k-O&42}}Cjg5`eZ-*&SNO6P_h(~etF6WCsrbXqwU|HM(j9VtqH_=&Xl$~4 zO6!!#+Dvp1RbAyO7|Aez34jPAJ!#@3fpQRdb4hTfr>BqTsW?kqXOp81p(JP+fjszm zpJjs_6w~C!A}`UYJTui7KYT;mK6K)TwRvdk%v_Q=G=Th)_RkH(==F)Xap!3~TiK1N z5w^8-gMci+`lurtIMjmGBW)&Hc%qG&TiTqMUuGd-{~bXg7#Q#S69h1~cz>Rqa6sLL zm!L58*jD0s24e^w2ei3d0%1AW0DyCV+?E@0URhJj2BnxD8A@{v*;E`_7Eyra&&bTE$J z#NxB%*tfUz^nC#K{;QES?0?nlFX` z7>(1OClH3g1jKU<4wFqb%Kdf%JXr`0I2=SSC4d|NogBy}Gm%5vauQMY10LCi-g@E_ z4H@vp{9vqY)a3Azc5WWV=~I&_lE3=hf)=@((O;@ZzaAck2dlBOKNu5JvvK8|wxTJ7 zOzc3Zt(Pb$;c%MMfx!%$^>r;63)n+L(7fx4VTCa`MWzu<0LPIyaWO~ zw*|i3LjxcE`H_U{2OK;v0{Z&I|9H6(Z+)^F?fP2$;L5Ou*IfMS?ZtTQ>{L8jZbnI) zg~s&KZEC!#xv6?=3kb4HJN5b@LD=5jP6m0HFhPRHNN@zknF8KM(+{n)458qh5!49; zj>AXb7>+nV8#-u^k4%CLEp_lNk^3Tm#(>8fO*GQAbYB3oXuapoc!oavkR_ zhZ3Cx%|WpZHu`1dFy0D0z@0fq2WLdvN;@BcmZh9$w|qf*FM^| z&2+SJnSFNL>f=#EZyta5!IL;MGpQH&wG)E^f5l@O1FW37QyQYAKuR18j359UID-?O z7odmEXPZfy`x^Rj8oB5}rh@`4!-$eTcsMK*T69__Isn_{AmzcK4*-|&s-y3>?>&ss zvEg{_%vcI~B0AwG-N#@IOauhLD8Ogj27-eYJZBsyDd`6Y2si_iGT%Uf<7!3_p~XQ5 zhPrWf%=+M?A3S<{0X*O`?>Ek+6vo>hKaIwIe|-P-X*CO9bDq4Oo~|(%#TY?wIJ5Z} zM>q#G7)#wo6C|7@2n3e{95TT3i4Tq<4}RqPY*KPqK88U9Kj0w-Z_WcfZ3it8cYf$I zn1|{6#-sIE+St=}kK!>x>@S51N^!P6l;;l1#;9iTi?C_Hrn;viw9vj~0;0t_Brf@Not;EoIj1Lu$h ztuqTcGLWnTOp6@Lg1#4W+^Ci!Ce956qv`FBpT$FcYV)0|=e1nX|G9#Kj&xO`Vnb1s zAK)9z4>Q zvr~FD(n@=0l66oh$r8750&r$H?VU9kO3?T`nV|3pz*o7J+dEW{WoMxQ(1$#9IaAP2 za5%uZ-*&-Ao$X`5`!^P2gcH`UtP}o=K;<2+k6zQ+AFK|5xa7C23i92oj| zVkS5?l3RZqo0^(Ryf|b>2^{Oi@Iw#3>BsE6?=!;kMQ#;UIO-?==d;8c?l3K}SiD$P_z|L-2Fp6aj&bk_>EG>`{|IzV8IM}Ji(iEZ!y^X~0v9ad%E zaGX0m9#dnwf&z~n1IIx>yaa(Z{qS=@4-TCUL=vhW=;#LsG`76`$x>9R!*Oy=RjB{I^g92@PQVZ)_2SCGkx_cM9Mn&zbr_X z8OcqVFst>?t5@c=*F@VqC-jB848(amP09n{0J4QXN&qK^h6WXoM%;b)ESB}(ILi8} zUjHp2y%!$VB2k*8wJoaM=G}KOhY<+oE2QozxSR;CY6Oy?FzhJN!vWo=4>sb;#S7d( zrPVs2zEn7Ne@zZB-<;?r+V{AnMVj^5v(UsxJPV62G z#p#)`*x1_C4$Dfctmvc60bSj8S9G7Dxkz6bPEYFpN9bb*_N46X6{~#6-&dVdQHTLu zEfpet8{JIv+>q8M7Z%r}uR0o&dJUpmz%Ml*7IrK!ICyZN3@I`8xs(=nxdeqQ@np)~ zEVKlMGfD7P*Ebaq1)(tzlX@L-eSTc4^*X!r6LMTMa&&vQ5i9GH`r@aNzGW08a|;J^!Q#?dO7l^j)!G2hRYf3r?Zsha`O%*pm}A<}B#|BH zRF2xltIUHXLD;nFETgRRMaI#&ZVDD=d1HRFU=Ue zW9Q1&EFMZLvT;av+^-7EcSAb4rAN#9Z&ewj)pYgNn+LI~Z56lfFUCYox9Mf}y=ec8 z950u-**c6X=O^Rz?3DJU>`Hk}+qGDzIwmfY{FkdTC+=z<TzLYHm!S@|F2mx zB(`_7(Mf;f+FMuG9XbAxMZ?3zcjljE-wVS>nx}z%xF6?F)%1Cs1a^|aIk7P}9(NzF zDlq+VP1%C-g734`ezpDB#|497#IR=?;GcIj6(FvluQKnHo~+i{L}Z^om&I%v=EF*s zQJF};Y9JE`zE8l*M(wWt<=cP)!j%+YLgqGOPjlm-ZbBI^=|&Y{CqU)V^1d#8Wj&1+ z1kQ#NXrDtdpCI?E42sRhuDTw_>e}eMqxp~jQ8hbW+(tZSm=L~l7VnRW=Pa(Q%|<;v z<&)a%j~N~sR(4rSS5oQI$MwSv{nH2jYv8zc0_avlR~yaBqIuaF>5BOCYXsM(k$oIY z^4ziz3_gP34q&*Z6R0bOo)`6xD#+ABy=K*es-l*kP%?qVvW3S_^rCMx&YwHa*0B^U z?x+bG3Yl{n%A!GCI0op?Tq!l%tu&O@wKa5EA4;+Hod5O2C5wG-Ot@cmfQ|wo<8YY& zNX8c=)Ro}6B`E$`*RZz3V*~%JYe;qeeG&ja!Mmj|PnZT~#*SwW4z_h?y}Gul$CP$@ zAU%8bYzjykK7|zh7B337wN6y6RnuIQy7Y7OFC|e%M)7WDb?ZPM9uz6dSL>7PNz)vX zZh)mVkE{jtmdS>Ej`nrY)DtFbjyD6ixx!&InZzb!8w?L%K1wnSW7~R$Oe59 z!_Z}nlbOc?FB#}~8K8^Ix${(elO_AKf|K(kTcn#yHw$e9g-05m`2YgrK+)zdzS(S~ z{{eWEF?93-0ZPiHZBz5ml7iG!u=W(~u|aJ>l!Ig$fvC%{y1wPwZEz-v;mDmdHjI%4 z3Md)D{B;E4%N;qwf6fXS^1SCN<}XAE#MSTwmYNDnVbGa9QhN58*k@InI_e z--zCqaPH{Io|gtDw~w=l0eq;rt^lnnPz?<`93!Qf2dWwpWB~I4R}JPWZZt6zUT7)5 zY~bV}m&_r{=Ln9Ftn=`AP_62J<;h3_w4$3*F5$DYb17S#F@8{nFRdo@4yDgB8Y2J< z*->_I^&k)xWe0ukFNlg@r21dv=TKkS>NT-WSh`wu0)}cF=3qX#bN60ah-XM~H6mcO z+VSV9SI(G|{BjV!iYz!u->cxT>DCivj!xR?spAte`~-&^y+;cRdJtv@7h6<{jG8}p zE@e}im_KjEyozyuO&S8jiQ;>MZ3X8{XU@q-Fu|b>9jgT`%_|Hsu106)=hJ-iXJyP^ zGXSrmn>mMhkYV+iprAZ``ZQhQ=jP75`sDkWg8z>ZC~?xPe}~M`%F1bF4&li&r`KeL z(9+Vf{-3XwYSr&Tb7h*InNFAXuk}#!NoL-8sE6eh-Db?6 zFd>_clKH2lN%?D=F}btU*)uafq<>Yb57>b;)KcyE55uWCptqvO(O=INS09dx`Y+L= z`sW!>*I)i_o14D>7lAoC-3ku>LVHBZ7i12>dJ+6D19zuY@^=6d00000NkvXXu0mjf Dy|EC8 literal 0 HcmV?d00001 diff --git a/src/components/basemap-selector/thumbs/btn-satellite@2x.png b/src/components/basemap-selector/thumbs/btn-satellite@2x.png new file mode 100755 index 0000000000000000000000000000000000000000..942c21605d4771573840caf5d72ea7b7bae5ddad GIT binary patch literal 11851 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 literal 0 HcmV?d00001 diff --git a/src/components/basemap-selector/thumbs/satellite.jpeg b/src/components/basemap-selector/thumbs/satellite.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..33c62c73b5ccc1262787704051ce8a19118bc9aa GIT binary patch literal 1625 zcmbW!c{tR090%~Dx8;b#oixdv(Su#Cu+Yut25?xRq0#Ke?wwyvmM*SJDb&S)7| zCP$+N<*MXLVQq}$*f1C(v7`3ceV+Ymci-RV`}ybl`F@|z>-p}!*?k8HSejax0uTrQ zKsXE79Rmme914TMpm0us!{OWrUL=AO{(bvUydb}zAjl7bXkiHvw2&AE1Vv>;#Uv%A zrKQm#SUIeeoP?CL)ZQZy&Q%0Af)9!0lM(`jr2cbup8y~d&;@j%5G=q2LZBdIw*!#o z%n65l4e)0mTu>NiN+gPBAE%)~0N{c^p+EcI;^g7L{m$fU~FP)W^Q3=d)m(4!O_Xt z>)d(o3qHPnq{~-A$f053aq-s@60hI*DLo@IEBj_nZeCGwNom==@{0R)^$!|o4;!1B z+uA#xKI`o2?im;y8h$zdDhD68hd@?)8^Km3j#pDu{iY`>>nB^C*@gt zw6C&%2aEk*vcF(|yG8+CD1`HQP!P}uHcdwz66~`0pKCkR&*sq!in0S;)3X`!zAjZGEZ_9gXob&?!)*SL>oA{=?{o338+}CuRm3@q4_A4M4acLPUZsd z^hk`2YQ>v``tk)_;3Bp=AtD5ENef`oBABD4K9McG=IiBni?q&rMkJ@5%d98UTB~*L zY9o5XnDn0BvRjEHRldqfQrwQZ;2YlsFFR@nzKs&2ylL6Wuj4Z3s2x9HvbJm{) zBerzjn;cxOXizfuty@B#yK3hGL#EbLqH>u9MzdBrMEMkcb=6dj8N2Dy#R5nEqy7Zr zA?o3l-x^%-iK7m@O)xMCoT%@f%1?s8sAfS6XN+DFhIPfM<9@Ua-*W51yR=oAo1(b5 zR}mz)kCe~uG2&LSlX!~_N&gqyxQOEU{xDTvmgBgIU^M0ip1CAl9qDlVieifnIi+vE zP%ZpWyQU<|-5%Q$(vZ^Z(9nxRZ(1E7-0l?BVhGKV-;SF_iFKGa`39`Gm zBnraIwt5k zGk5#hx9HkK?E$=kvRaW{(fRq|UlakOk#LH6NZ^kx1qlro==8et4^ypIF{eGEyVUi$ z`)mt}HWovd7cBYq%kz*%Td(DRa9G}v*4sK+yA;aADN|h=rI;`*6)sYOFE`5XgXhG2iOj)RujH+^kp7D!EcJ3 zSvH2^wIL08DO4XH4mnt;`e22-`^h!ixJGtgO(j@$4BPTUTH5^#83PN(>?5`w+jd7V zVrs&zs!2{+ztvz{JIX&xgXnnz9!5JBux07+a!4E z`%*;k9IYhKFZ{|-h<#04(@DM+&0e&HPvF|8JjZp@aV|64zEVHuam&}I%$FY3z!K&% z>?=JU-p;fN5J#L9mDmN6!<25*gtxpd){fDi6N7NdD!~SwKZf_Do^Ayy^t@4 { + const { children, + isDisabled, + hasBackground, + isTransparent, + isGrey, + hasContrast, + isActive, + ...domProps } = props; + + return ( + + ); +}; diff --git a/src/components/button/index.js b/src/components/button/index.js new file mode 100644 index 000000000..930651d36 --- /dev/null +++ b/src/components/button/index.js @@ -0,0 +1,3 @@ +import component from './component'; + +export default component; diff --git a/src/components/button/stories.jsx b/src/components/button/stories.jsx new file mode 100644 index 000000000..decabf5e8 --- /dev/null +++ b/src/components/button/stories.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import Button from './component'; +import styles from './style.module.scss'; + +storiesOf('Button/Background/Primary', module) + .addParameters({ options: { theme: styles.contrast } }) + .add('Active', () => ( + + )) + .add('Disabled', () => ( + + )); + +storiesOf('Button/Background/Contrast', module) + .addParameters({ options: { theme: styles.contrast } }) + .add('Active', () => ( + + )) + .add('Disabled', () => ( + + )); + +storiesOf('Button/Transparent/Primary', module) + .add('Active', () => ( + + )) + .add('Disabled', () => ( + + )); + +storiesOf('Button/Transparent/Contrast', module) + .add('Active', () => ( + + )) + .add('Disabled', () => ( + + )); + +storiesOf('Button/Transparent/Greys', module) + .add('Active', () => ( + + )) + .add('Disabled', () => ( + + )); diff --git a/src/components/button/style.module.scss b/src/components/button/style.module.scss new file mode 100644 index 000000000..fb68eaca5 --- /dev/null +++ b/src/components/button/style.module.scss @@ -0,0 +1,61 @@ +@import 'styles/vars'; + +.button { + padding: 0 20px; + min-width: 116px; + min-height: 30px; + background-color: transparent; + border-radius: 15px; + border: 2px solid rgba($primary, 0.2); + color: $primary; + font-size: $body-font-size; + font-weight: 600; + + transition: all 0.25s ease; + + &:hover { + border-color: rgba($primary, 0.4); + cursor: pointer; + } + + &.background { + background-color: $primary; + color:$white; + + &.contrast { + background-color: $white; + color: $primary; + border-color: $white; + } + } + + &.transparent { + border-color: rgba(0, 133, 127, 0.4); + background-color: transparent; + color: $primary; + + &:hover { + border: 2px solid rgba(0, 133, 127, 0.4); + } + + &.contrast { + color: $white; + } + &.grey { + border-color: rgba(0,0,0,0.2); + color: $body-color; + &:hover { + border-color: rgba(0,0,0,0.4); + } + } + } + + &.disabled { + cursor: default; + pointer-events: none; + opacity: 0.2; + } + @media screen and (max-width: map-get($breakpoints, md)) { + min-width: 0; + } +} diff --git a/src/components/buttonGroup/component.js b/src/components/buttonGroup/component.js new file mode 100644 index 000000000..23d7f12bc --- /dev/null +++ b/src/components/buttonGroup/component.js @@ -0,0 +1,12 @@ +import React from 'react'; +import ButtonGroup from 'react-bootstrap/ButtonGroup'; +import styles from './style.module.scss'; + +export default (props) => { + const { children } = props; + return ( + + {children} + + ); +}; diff --git a/src/components/buttonGroup/index.js b/src/components/buttonGroup/index.js new file mode 100644 index 000000000..930651d36 --- /dev/null +++ b/src/components/buttonGroup/index.js @@ -0,0 +1,3 @@ +import component from './component'; + +export default component; diff --git a/src/components/buttonGroup/stories.jsx b/src/components/buttonGroup/stories.jsx new file mode 100644 index 000000000..af955a6ef --- /dev/null +++ b/src/components/buttonGroup/stories.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import ButtonGroup from './component'; +import Button from '../button/component'; + +storiesOf('Button group', module) + .add('buttons bar', () => ( + + + + + + )); diff --git a/src/components/buttonGroup/style.module.scss b/src/components/buttonGroup/style.module.scss new file mode 100644 index 000000000..66edba931 --- /dev/null +++ b/src/components/buttonGroup/style.module.scss @@ -0,0 +1,27 @@ +@import 'styles/vars'; + +.container { + border: 2px solid rgba(0,0,0,0.2); + display: inline-flex; + position: relative; + height: 26px; + border-radius: 15px; + + button { + display: block; + position: relative; + margin: -2px; + border: none; + text-align: center; + color: $body-color; + &:focus { + text-align: center; + border: 2px solid white; + margin: -2px; + height: 30px; + background-color: $white; + color: $primary; + border-bottom: 3px solid white;; + } + } +} diff --git a/src/components/chart/component.js b/src/components/chart/component.js new file mode 100644 index 000000000..f00479245 --- /dev/null +++ b/src/components/chart/component.js @@ -0,0 +1,283 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import maxBy from 'lodash/maxBy'; +import max from 'lodash/max'; +import { + Line, + Bar, + Cell, + Area, + Pie, + XAxis, + YAxis, + CartesianGrid, + CartesianAxis, + Tooltip, + Legend, + ResponsiveContainer, + ComposedChart, + PieChart, + Label +} from 'recharts'; + +import { stack, clearStack, addComponent } from './rechart-components'; +import ChartTick from './tick'; +import { + allowedKeys, + defaults +} from './constants'; + +import styles from './style.module.scss'; + +const rechartCharts = new Map([ + ['pie', PieChart], + ['composed', ComposedChart] +]); + +class Chart extends PureComponent { + static propTypes = { + data: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + config: PropTypes.shape({}).isRequired, + className: PropTypes.string, + handleMouseMove: PropTypes.func, + handleMouseLeave: PropTypes.func + }; + + static defaultProps = { + className: '', + handleMouseMove: null, + handleMouseLeave: null + } + + findMaxValue = (data, config) => { + const { yKeys } = config; + const maxValues = []; + + Object.keys(yKeys).forEach((key) => { + Object.keys(yKeys[key]).forEach((subKey) => { + if (data.some(d => d.key)) { + maxValues.push(maxBy(data, subKey)[subKey]); + } + }); + }); + + return max(maxValues); + }; + + render() { + const { + data, + config, + handleMouseMove, + handleMouseLeave + } = this.props; + + const { + margin = { top: 20, right: 0, left: 50, bottom: 0 }, + padding = { top: 0, right: 0, left: 0, bottom: 0 }, + type = 'composed', + height, + layout = 'horizontal', + gradients, + patterns, + ...content + } = config; + + const { + xKey, + yKeys, + xAxis, + yAxis, + cartesianGrid, + cartesianAxis, + tooltip, + legend, + unit, + unitFormat + } = content; + + clearStack(); + + const { lines, bars, areas, pies } = yKeys; + const maxYValue = this.findMaxValue(data, config); + + const RechartChart = rechartCharts.get(type); + + Object.entries(content).forEach(entry => { + const [key, definition] = entry; + if (allowedKeys.includes(key)) { + addComponent(key, definition); + } + }); + + return ( +
+ + + + {gradients && Object.keys(gradients).map(key => ( + + {gradients[key].stops && Object.keys(gradients[key].stops).map(sKey => ( + + )) + } + + )) + } + + {patterns && Object.keys(patterns).map(key => ( + + {patterns[key].children && Object.keys(patterns[key].children).map((iKey) => { + const { tag } = patterns[key].children[iKey]; + + return React.createElement( + tag, + { + key: iKey, + ...patterns[key].children[iKey] + } + ); + }) + } + + )) + } + + { stack } + + {cartesianGrid && ( + + )} + + {cartesianAxis && ( + + )} + + {xAxis && ( + + )} + + {yAxis && ( + value)} + fill="#AAA" + /> + )} + {...yAxis} + /> + )} + + {areas && Object.keys(areas).map(key => ( + + ))} + + {bars && Object.keys(bars).map(key => ( + + {!!bars[key].label && + ))} + + {lines && Object.keys(lines).map(key => ( + + ))} + + {pies && ( + Object.keys(pies).map(key => ( + + {data.map(item => ( + + ))} + + )) + )} + + {layout === 'vertical' && xAxis && ( + + )} + + {tooltip && ( + + )} + + {legend && ( + + )} + + +
+ ); + } +} + +export default Chart; diff --git a/src/components/chart/constants.js b/src/components/chart/constants.js new file mode 100644 index 000000000..5a0e7f9c3 --- /dev/null +++ b/src/components/chart/constants.js @@ -0,0 +1,11 @@ +export const allowedKeys = [ + 'referenceAreas', + 'referenceLines' +]; + +export const defaults = { + cartesianGrid: { + strokeDasharray: '4 4', + stroke: '#d6d6d9' + } +} \ No newline at end of file diff --git a/src/components/chart/index.js b/src/components/chart/index.js new file mode 100644 index 000000000..2e331cd27 --- /dev/null +++ b/src/components/chart/index.js @@ -0,0 +1 @@ +export {default} from './component'; diff --git a/src/components/chart/rechart-components/defaults.js b/src/components/chart/rechart-components/defaults.js new file mode 100644 index 000000000..e69de29bb diff --git a/src/components/chart/rechart-components/index.js b/src/components/chart/rechart-components/index.js new file mode 100644 index 000000000..1798fc250 --- /dev/null +++ b/src/components/chart/rechart-components/index.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { isObject, isArray } from 'lodash'; +import { + CartesianGrid, + CartesianAxis, + ReferenceLine, + ReferenceArea, + Line +} from 'recharts'; + +const rechartsComponentsMap = new Map([ + ['referenceAreas', ReferenceArea], + ['referenceLines', ReferenceLine], + ['cartesianGrid', CartesianGrid], + ['cartesianAxis', CartesianAxis] +]); + +export let stack = []; + +export function clearStack() { + stack = []; +} + +export function addComponent(type, options) { + if (!rechartsComponentsMap.has(type)) { + return null; + } + + const Component = rechartsComponentsMap.get(type); + + if (isArray(options)) { + options.forEach((itemOptions, index) => stack.push()) + } else if (isObject(options)) { + stack.push(); + } + + return null; +}; \ No newline at end of file diff --git a/src/components/chart/stories.jsx b/src/components/chart/stories.jsx new file mode 100644 index 000000000..227a61ca3 --- /dev/null +++ b/src/components/chart/stories.jsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import TooltipComponent from './tooltip/component'; +import TickComponent from './tick/component'; + +storiesOf('Chart', module) + .add('Tooltip', () => ( + + )) + .add('Tick', () => ( + + )); diff --git a/src/components/chart/style.module.scss b/src/components/chart/style.module.scss new file mode 100644 index 000000000..fc9b9c63b --- /dev/null +++ b/src/components/chart/style.module.scss @@ -0,0 +1,4 @@ +.chart { + width: 100%; + height: 250px; +} diff --git a/src/components/chart/tick/component.js b/src/components/chart/tick/component.js new file mode 100644 index 000000000..d11dc91bd --- /dev/null +++ b/src/components/chart/tick/component.js @@ -0,0 +1,62 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +class Tick extends PureComponent { + static propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.shape({}), + dataMax: PropTypes.number, + unit: PropTypes.string.isRequired, + unitFormat: PropTypes.func.isRequired, + fill: PropTypes.string.isRequired, + backgroundColor: PropTypes.string + } + + static defaultProps = { + x: 0, + y: 0, + dataMax: Infinity, + payload: {}, + backgroundColor: '' + } + + render() { + const { + x, + y, + payload, + dataMax, + unit, + unitFormat, + fill, + backgroundColor + } = this.props; + + const tickValue = payload && payload.value; + const formattedTick = tickValue ? unitFormat(tickValue) : 0; + const tick = tickValue >= dataMax ? `${formattedTick}${unit}` : formattedTick; + return ( + + + + + + + + Hello + + {tick} + + + ); + } +} + +export default Tick; diff --git a/src/components/chart/tick/index.js b/src/components/chart/tick/index.js new file mode 100644 index 000000000..2069ac0dc --- /dev/null +++ b/src/components/chart/tick/index.js @@ -0,0 +1,3 @@ +import TickComponent from './component'; + +export default TickComponent; diff --git a/src/components/chart/tooltip/component.js b/src/components/chart/tooltip/component.js new file mode 100644 index 000000000..4d5933624 --- /dev/null +++ b/src/components/chart/tooltip/component.js @@ -0,0 +1,75 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +// Styles +import styles from './style.module.css'; + +class Tooltip extends PureComponent { + static propTypes = { + payload: PropTypes.arrayOf(PropTypes.shape({})), + settings: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + hideZeros: PropTypes.bool + }; + + static defaultProps = { + payload: [], + hideZeros: false + } + + getValue = (item, value) => { + const { format, suffix = '', preffix = '' } = item; + let val = value; + + if (format && typeof format === 'function') { + val = format(val); + } + + return `${preffix}${val}${suffix}`; + } + + render() { + const { payload, settings, hideZeros } = this.props; + const values = payload && payload.length > 0 && payload[0].payload; + return ( +
+ {settings && settings.length && ( +
+ {settings.map( + d => (hideZeros && !values[d.key] ? null : ( +
+ {/* LABEL */} + {(d.label || d.labelKey) && ( +
+ {d.color && ( +
+ )} + + {d.key === 'break' ? ( + {d.label} + ) : ( + {d.label || values[d.labelKey]} + )} +
+ )} + + {/* UNIT */} +
+ {this.getValue(d, values[d.key])} +
+
+ )) + )} +
+ )} +
+ ); + } +} + +export default Tooltip; diff --git a/src/components/chart/tooltip/index.js b/src/components/chart/tooltip/index.js new file mode 100644 index 000000000..5fafc60cd --- /dev/null +++ b/src/components/chart/tooltip/index.js @@ -0,0 +1,3 @@ +import TooltipComponent from './component'; + +export default TooltipComponent; diff --git a/src/components/chart/tooltip/style.module.css b/src/components/chart/tooltip/style.module.css new file mode 100644 index 000000000..42607836a --- /dev/null +++ b/src/components/chart/tooltip/style.module.css @@ -0,0 +1,46 @@ +.chart_tooltip { + padding: 10px 20px; + box-shadow: 0 0 6px 0 rgba(0, 0, 0, 0.19); + background-color: #FFF; + color: gray; + border-radius: 2px; + font-size: 14px; +} + +.data_line { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; +} + +.data_line.right { + justify-content: flex-end; + text-align: right; +} + +.data_label { + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: flex-start; + margin-right: 20px; +} + +.break_label { + font-size: 14px; + font-style: italic; + flex-direction: row; + padding-top: 5px; + padding-bottom: 5px; +} + +.data_color { + width: 12px; + height: 12px; + min-height: 12px; + min-width: 12px; + border-radius: 50%; + margin-right: 5px; + margin-top: 5px; +} diff --git a/src/components/datepicker/component.js b/src/components/datepicker/component.js new file mode 100644 index 000000000..843d762df --- /dev/null +++ b/src/components/datepicker/component.js @@ -0,0 +1,71 @@ +import React, { PureComponent } from 'react'; +import { createPortal } from 'react-dom'; +import PropTypes from 'prop-types'; +import ReactDatePicker, { CalendarContainer } from 'react-datepicker'; +import classnames from 'classnames'; + +import DatepickerInput from './input'; + +import styles from './style.module.scss'; + +class Datepicker extends PureComponent { + renderCalendarContainer = ({ children }) => { + return createPortal( + + {children} + + , document.body); + }; + + render() { + const { className, onDateChange, settings, theme, date, inline } = this.props; + const { minDate, maxDate } = settings; + + return ( +
{ this.ref = ref; }} + className={classnames(styles.Datepicker, theme, className, { [styles._inline]: inline})} + > + } + // Popper + popperContainer={this.renderCalendarContainer} + popperPlacement="bottom-start" + popperClassName={styles.DatepickerPopper} + popperModifiers={{ + flip: { + enabled: false + }, + offset: { + enabled: true, + offset: '0px, -15px' + }, + preventOverflow: { + enabled: true, + escapeWithReference: false, // force popper to stay in viewport (even when input is scrolled out of view) + boundariesElement: 'viewport' + } + }} + // Func + onSelect={onDateChange} + // renderCustomHeader={this.renderCalendarHeader} + /> +
+ ); + } +} + +Datepicker.propTypes = { + className: PropTypes.string, + theme: PropTypes.string, + date: PropTypes.object, + onDateChange: PropTypes.func.isRequired, + settings: PropTypes.object +}; + +export default Datepicker; \ No newline at end of file diff --git a/src/components/datepicker/index.js b/src/components/datepicker/index.js new file mode 100644 index 000000000..601c659fe --- /dev/null +++ b/src/components/datepicker/index.js @@ -0,0 +1 @@ +export {default} from './component'; \ No newline at end of file diff --git a/src/components/datepicker/input/component.js b/src/components/datepicker/input/component.js new file mode 100644 index 000000000..dbc3418c5 --- /dev/null +++ b/src/components/datepicker/input/component.js @@ -0,0 +1,42 @@ +import React, { PureComponent } from 'react'; +import classnames from 'classnames'; + +import styles from './style.module.scss'; + +class DatepickerInput extends PureComponent { + state = { + focus: false + } + + onFocus = (e) => { + const { onFocus } = this.props; + + this.setState({ focus: true }); + onFocus(e); + } + + onBlur = (e) => { + const { onBlur } = this.props; + + this.setState({ focus: false }); + onBlur(e); + } + + render () { + const { value, onClick } = this.props; + const { focus } = this.state; + + return ( + + ) + } +} + +export default DatepickerInput; \ No newline at end of file diff --git a/src/components/datepicker/input/index.js b/src/components/datepicker/input/index.js new file mode 100644 index 000000000..76f86e1e7 --- /dev/null +++ b/src/components/datepicker/input/index.js @@ -0,0 +1 @@ +export { default } from './component'; \ No newline at end of file diff --git a/src/components/datepicker/input/style.module.scss b/src/components/datepicker/input/style.module.scss new file mode 100644 index 000000000..d545fd8cd --- /dev/null +++ b/src/components/datepicker/input/style.module.scss @@ -0,0 +1,33 @@ +@import '~styles/vars'; + +.DatepickerInput { + display: inline-block; + text-align: center; + text-transform: uppercase; + font-weight: bold; + cursor: pointer; + border-width: 0; + + &:after { + content: ""; + position: absolute; + bottom: -7px; + right: calc(50% - 3px); + width: 0; + height: 0; + margin: -3px 0 0; + border-style: solid; + border-width: 6px 6px 0px 6px; + border-color: $primary transparent transparent transparent; + } + + &._focus, &:focus { + outline: none; + color: $primary; + + &:after { + border-width: 0px 6px 6px 6px; + border-color: transparent transparent $primary transparent; + } + } +} \ No newline at end of file diff --git a/src/components/datepicker/style.module.scss b/src/components/datepicker/style.module.scss new file mode 100644 index 000000000..0d485d89b --- /dev/null +++ b/src/components/datepicker/style.module.scss @@ -0,0 +1,16 @@ +@import '~styles/vars'; + +.Datepicker { + &._inline { + display: inline-block; + } + + .react-datepicker__day-name { + font-family: $font-family; + } +} + +.DatepickerPopper { + z-index: 10!important; +} + diff --git a/src/components/header/bg-fixed.svg b/src/components/header/bg-fixed.svg new file mode 100644 index 000000000..bcfa3b45a --- /dev/null +++ b/src/components/header/bg-fixed.svg @@ -0,0 +1,25 @@ + + + + bg-fixed + Created with Sketch. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/header/bg-shape.svg b/src/components/header/bg-shape.svg new file mode 100644 index 000000000..4c8bbc1cf --- /dev/null +++ b/src/components/header/bg-shape.svg @@ -0,0 +1,34 @@ + + + + Group 19 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/header/component.js b/src/components/header/component.js new file mode 100644 index 000000000..efdbddbfc --- /dev/null +++ b/src/components/header/component.js @@ -0,0 +1,86 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import MediaQuery from 'react-responsive'; +import { breakpoints } from 'utils/responsive'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faSearch } from '@fortawesome/free-solid-svg-icons'; +import background from './bg-shape.svg'; +import fixedBackground from './bg-fixed.svg'; +import styles from './style.module.scss'; + +class Header extends PureComponent { + static propTypes = { + sticky: PropTypes.bool, + location: PropTypes.shape({ + name: PropTypes.string + }), + openSearchPanel: PropTypes.func + } + + static defaultProps = { + sticky: false, + location: { name: 'Location name' }, + openSearchPanel: () => null + } + + clickHandler = () => { + const { openSearchPanel } = this.props; + openSearchPanel(); + } + + + render() { + const { location, sticky } = this.props; + let stylesOverride = { fontSize: 60, lineHeight: 0.85 }; + + if (location && location.name.length > 10) stylesOverride = { fontSize: 45, lineHeight: 1 }; + if (location && location.name.length > 30) stylesOverride = { fontSize: 30, lineHeight: 1 }; + + return ( +
+ Background + Background +
+ + {location && ( + + )} +
+
+ ); + } +} + +export default Header; diff --git a/src/components/header/index.js b/src/components/header/index.js new file mode 100644 index 000000000..722619c18 --- /dev/null +++ b/src/components/header/index.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { currentLocation } from 'modules/locations/selectors'; +import { openSearchPanel } from 'modules/locations/actions'; +import Component from './component'; + +const mapStateToProps = state => ({ + location: currentLocation(state) +}); + +const mapDispatchToProps = { + openSearchPanel +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/header/stories.jsx b/src/components/header/stories.jsx new file mode 100644 index 000000000..7e670a599 --- /dev/null +++ b/src/components/header/stories.jsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; + +import Component from './component'; + +storiesOf('Header', module) + .add('header', () => ( + + )); diff --git a/src/components/header/style.module.scss b/src/components/header/style.module.scss new file mode 100644 index 000000000..839755886 --- /dev/null +++ b/src/components/header/style.module.scss @@ -0,0 +1,98 @@ +@import 'styles/vars'; + +.header { + display: flex; + margin-bottom: 50px; + @media screen and (max-width: map-get($breakpoints, md)) { + margin-bottom: 25px; + } + + .bg, + .bgFixed { + display: block; + position: absolute; + top: 0; + left: 0; + pointer-events: none; + overflow: visible; + z-index: -9; + &.isHidden { + display: none; + } + @media screen and (max-width: map-get($breakpoints, md)) { + display: none; + } + } + + .bgFixed { + position: fixed; + z-index: 100; + @media screen and (max-width: map-get($breakpoints, md)) { + display: none; + } + } + + .searchBar { + margin-top: 80px; + z-index: 300; + width: 100%; + display: flex; + &.fixed { + position: fixed; + top: -65px; + } + @media screen and (max-width: map-get($breakpoints, md)) { + margin-top: 20px; + font-size: 35px; + } + } + + .titleBtn { + border: 0; + padding: 0; + appearance: none; + } + + .title { + padding: 0; + margin: 0; + border: 0; + background: transparent; + color: $body-color; + font-size: 60px; + font-weight: 300; + line-height: 0.85; + text-align: left; + + @media screen and (max-width: map-get($breakpoints, md)) { + font-size: 35px; + } + } + + .searchButton { + width: 60px; + height: 60px; + margin-right: 20px; + border: 0; + border-radius: 100%; + background: white; + text-align: center; + cursor: pointer; + box-shadow: $box-shadow; + + :global(.svg-inline--fa path) { + fill: $primary; + } + @media screen and (max-width: map-get($breakpoints, md)) { + height: 50px; + width: 50px; + } + svg { + @media screen and (max-width: map-get($breakpoints, md)) { + height: 30px; + width: 30px; + vertical-align: 0; + } + } + } +} diff --git a/src/components/language-selector/component.js b/src/components/language-selector/component.js new file mode 100644 index 000000000..39f387f38 --- /dev/null +++ b/src/components/language-selector/component.js @@ -0,0 +1,88 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import MediaQuery from 'react-responsive'; +import { breakpoints } from 'utils/responsive'; +import ButtonGroup from 'components/buttonGroup'; +import Button from 'components/button'; + +class LanguageSelect extends PureComponent { + static propTypes = { + language: PropTypes.string, + data: PropTypes.arrayOf(PropTypes.shape({ + name: PropTypes.string, + code: PropTypes.string, + })), + fetchLanguages: PropTypes.func.isRequired, + setCurrentLanguage: PropTypes.func.isRequired, + } + + static defaultProps = { + language: 'en', + data: null, + } + + componentDidMount() { + const { fetchLanguages, setCurrentLanguage } = this.props; + const { Transifex } = window; + + if (typeof window !== 'undefined') { + fetchLanguages(); + if (Transifex && typeof Transifex !== 'undefined') { + Transifex.live.onReady(() => { + const { code } = Transifex.live.getSourceLanguage(); + const langCode = Transifex.live.detectLanguage(); + + Transifex.live.translateTo(code); + Transifex.live.translateTo(langCode); + + setCurrentLanguage(langCode); + }); + } + } + } + + handleChange = ({ langCode }) => { + const { Transifex } = window; + const { setCurrentLanguage } = this.props; + Transifex.live.translateTo(langCode); + setCurrentLanguage(langCode); + } + + render() { + const { language, data } = this.props; + + const options = data.map(lang => ({ + label: lang.name, + value: lang.code, + code: (lang.code.split('_')[0]).toUpperCase() + })); + const currentValue = options.find(o => o.value === language); + if (!data || !currentValue) return null; + + return ( + + + {options.map(o => ( + + ))} + + ); + } +} + + +export default LanguageSelect; diff --git a/src/components/language-selector/index.js b/src/components/language-selector/index.js new file mode 100644 index 000000000..63d75b791 --- /dev/null +++ b/src/components/language-selector/index.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import { fetchLanguages, setCurrentLanguage } from 'modules/languages/actions'; +import Component from './component'; + +const mapStateToProps = state => ({ + language: state.languages.current, + data: state.languages.list, +}); + +const mapDispatchToProps = { + fetchLanguages, + setCurrentLanguage +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/language-selector/stories.jsx b/src/components/language-selector/stories.jsx new file mode 100644 index 000000000..af955a6ef --- /dev/null +++ b/src/components/language-selector/stories.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import ButtonGroup from './component'; +import Button from '../button/component'; + +storiesOf('Button group', module) + .add('buttons bar', () => ( + + + + + + )); diff --git a/src/components/language-selector/style.module.scss b/src/components/language-selector/style.module.scss new file mode 100644 index 000000000..a386d7e7a --- /dev/null +++ b/src/components/language-selector/style.module.scss @@ -0,0 +1,26 @@ +@import 'styles/vars'; + +.container { + border: 2px solid rgba(0,0,0,0.2); + display: inline-flex; + position: relative; + border-radius: 15px; + + button { + display: block; + position: relative; + margin: -2px; + border: none; + text-align: center; + color: $body-color; + + &:focus { + text-align: center; + border: 2px solid white; + margin: -2px; + background-color: $white; + color: $primary; + border-bottom: 3px solid white; + } + } +} diff --git a/src/components/layout/desktop/component.js b/src/components/layout/desktop/component.js new file mode 100644 index 000000000..366626f4a --- /dev/null +++ b/src/components/layout/desktop/component.js @@ -0,0 +1,18 @@ +import React from 'react'; +import Widgets from 'components/widgets'; +import Map from 'components/map'; +import Sidebar from 'components/sidebar'; +import styles from '../style.module.scss'; + +const DesktopLayout = () => ( +
+ + + +
+ +
+
+); + +export default DesktopLayout; diff --git a/src/components/layout/desktop/index.js b/src/components/layout/desktop/index.js new file mode 100644 index 000000000..5081bc3d6 --- /dev/null +++ b/src/components/layout/desktop/index.js @@ -0,0 +1,3 @@ +import DesktopLayout from './component'; + +export default DesktopLayout; diff --git a/src/components/layout/mobile/component.js b/src/components/layout/mobile/component.js new file mode 100644 index 000000000..57f2523b7 --- /dev/null +++ b/src/components/layout/mobile/component.js @@ -0,0 +1,33 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import Widgets from 'components/widgets'; +import Sidebar from 'components/sidebar'; +import Map from 'components/map'; +import ViewSelector from 'components/view-selector'; +import styles from '../style.module.scss'; + +class MobileLayout extends PureComponent { + static propTypes = { + mapView: PropTypes.bool.isRequired + } + + render() { + const { mapView } = this.props; + return ( +
+ {!mapView && ( + + + + )} + {mapView && ( +
+ +
)} + +
+ ); + } +} + +export default MobileLayout; diff --git a/src/components/layout/mobile/index.js b/src/components/layout/mobile/index.js new file mode 100644 index 000000000..381b87a23 --- /dev/null +++ b/src/components/layout/mobile/index.js @@ -0,0 +1,8 @@ +import { connect } from 'react-redux'; +import Component from './component'; + +const mapStateToProps = state => ({ + mapView: state.app.mobile.mapView +}); + +export default connect(mapStateToProps)(Component); diff --git a/src/components/layout/style.module.scss b/src/components/layout/style.module.scss new file mode 100644 index 000000000..5ba81ce7b --- /dev/null +++ b/src/components/layout/style.module.scss @@ -0,0 +1,10 @@ +@import 'styles/vars'; + +.vis { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + z-index: 1; +} diff --git a/src/components/link/component.js b/src/components/link/component.js new file mode 100644 index 000000000..753bad80e --- /dev/null +++ b/src/components/link/component.js @@ -0,0 +1,34 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { jsonToCSV } from 'utils/jsonParsers'; +import { CSVLink } from 'react-csv'; +import styles from './style.module.scss'; + + +class DownloadLink extends PureComponent { + static propTypes = { + data: PropTypes.arrayOf(PropTypes.shape({})), + slug: PropTypes.string + } + + static defaultProps = { + data: null, + slug: null + } + + render() { + const { data, slug } = this.props; + const csvData = jsonToCSV(data); + return ( + + Download raw data + + ); + } +} + +export default DownloadLink; diff --git a/src/components/link/index.js b/src/components/link/index.js new file mode 100644 index 000000000..574d0d818 --- /dev/null +++ b/src/components/link/index.js @@ -0,0 +1,3 @@ +import DownloadLink from './component'; + +export default DownloadLink; diff --git a/src/components/link/stories.jsx b/src/components/link/stories.jsx new file mode 100644 index 000000000..d18c1eb35 --- /dev/null +++ b/src/components/link/stories.jsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withProvider } from 'utils/storybookProvider'; +import DownloadLink from './component'; + +storiesOf('Download link', module) + .addDecorator(withProvider) + .add('data', () => ( + + )); diff --git a/src/components/link/style.module.scss b/src/components/link/style.module.scss new file mode 100644 index 000000000..7c7bbef11 --- /dev/null +++ b/src/components/link/style.module.scss @@ -0,0 +1,9 @@ +@import 'styles/vars'; + + +.downloadButton { + display: inline-block; + margin-top: 20px; + color: rgba($color: #000000, $alpha: 0.4); + text-decoration: underline; +} diff --git a/src/components/location-modal/component.js b/src/components/location-modal/component.js new file mode 100644 index 000000000..0c3a0dc0b --- /dev/null +++ b/src/components/location-modal/component.js @@ -0,0 +1,162 @@ +import React, { PureComponent, Fragment } from 'react'; +import PropTypes from 'prop-types'; +import Link from 'redux-first-router-link'; +import classnames from 'classnames'; +import Modal from 'components/modal'; +import MediaQuery from 'react-responsive'; +import { breakpoints } from 'utils/responsive'; +import HighlightedPlaces from 'components/widget/templates/highlighted-places/component'; +import highlightedPlacesConfig from 'components/widget/templates/highlighted-places/config'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import styles from './style.module.scss'; + +class LocationSelector extends PureComponent { + static propTypes = { + isOpened: PropTypes.bool, + currentLocation: PropTypes.shape({ + name: PropTypes.string + }), + locations: PropTypes.arrayOf(PropTypes.shape({})), + highlightedPlaces: PropTypes.arrayOf(PropTypes.shape({})), + closeSearchPanel: PropTypes.func + } + + static defaultProps = { + isOpened: false, + currentLocation: { name: 'Location name' }, + locations: [], + highlightedPlaces: null, + closeSearchPanel: () => null + } + + state = { + searchTerm: null + }; + + componentWillReceiveProps(nextProps) { + if (nextProps.isOpened) this.resetTerm(); + } + + closeModal = () => { + const { closeSearchPanel } = this.props; + + closeSearchPanel(); + this.resetTerm(); + } + + resetTerm = () => this.setState({ searchTerm: null }) + + updateSearchTerm = (e) => { + if (e.currentTarget.value === '') { + this.resetTerm(); + } else { + this.setState({ searchTerm: e.currentTarget.value }); + } + } + + render() { + const { isOpened, currentLocation, locations, highlightedPlaces } = this.props; + if (!currentLocation) return null; + + const { searchTerm } = this.state; + const locationsData = searchTerm + ? locations.filter(l => new RegExp(searchTerm, 'i').test(l.name)) + : locations; + + return ( + + + +
+
+ +
+ {highlightedPlaces && ( + + )} +
    +
  • + Worldwide +
  • + {locationsData.map(location => ( +
  • + {location.location_type === 'aoi' + && {location.name}} + {location.location_type === 'country' + && {location.name}} + {location.location_type === 'wdpa' + && {location.name}} +
  • + ))} +
+
+ +
+
+ + +
+
+ +
+ {highlightedPlaces && ( + + )} +
    +
  • + Worldwide +
  • + {locationsData.map(location => ( +
  • + {location.location_type === 'aoi' + && {location.name}} + {location.location_type === 'country' + && {location.name}} + {location.location_type === 'wdpa' + && {location.name}} +
  • + ))} +
+
+ +
+
+
+ ); + } +} + +export default LocationSelector; diff --git a/src/components/location-modal/index.js b/src/components/location-modal/index.js new file mode 100644 index 000000000..e5eb168a5 --- /dev/null +++ b/src/components/location-modal/index.js @@ -0,0 +1,17 @@ +import { connect } from 'react-redux'; +import { currentLocation, highlightedPlaces } from 'modules/locations/selectors'; +import { closeSearchPanel } from 'modules/locations/actions'; +import Component from './component'; + +const mapStateToProps = state => ({ + isOpened: state.locations.isOpened, + currentLocation: currentLocation(state), + highlightedPlaces: highlightedPlaces(state), + locations: state.locations.list +}); + +const mapDispatchToProps = { + closeSearchPanel +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/location-modal/stories.jsx b/src/components/location-modal/stories.jsx new file mode 100644 index 000000000..722490f36 --- /dev/null +++ b/src/components/location-modal/stories.jsx @@ -0,0 +1,11 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withProvider } from 'utils/storybookProvider'; +import LocationSelector from './component'; + + +storiesOf('Location modal', module) + .addDecorator(withProvider) + .add('open', () => ( + + )); diff --git a/src/components/location-modal/style.module.scss b/src/components/location-modal/style.module.scss new file mode 100644 index 000000000..1f3cec955 --- /dev/null +++ b/src/components/location-modal/style.module.scss @@ -0,0 +1,133 @@ +@import 'styles/vars'; + +.location { + $padding: 30px; + $border-radius: 20px; + + position: absolute; + max-width: 540px; + width: 100%; + top: 50px; + left: 50px; + bottom: 50px; + + z-index: 100; + + &.mobile { + top: 10px; + bottom: 10px; + left: 10px; + right: 10px; + width: inherit; + } + + .content { + display: flex; + flex-direction: column; + + position: absolute; + padding: $padding; + width: 100%; + height: 100%; + + border-radius: $border-radius; + background: white; + + box-shadow: $box-shadow; + box-sizing: border-box; + + overflow: auto; + z-index: 1; + &.mobile { + position: absolute; + right: 20px; + } + } + + .searchInput { + flex: 1; + + width: 100%; + padding: 0; + margin: 0; + border: 0; + background: transparent; + + caret-color: $primary; + color: $body-color; + font-size: 30px; + font-weight: 300; + line-height: 50px; + + &:focus { + outline: 0; + } + + &::placeholder { + color: $body-color; + } + } + + .searchButton { + position: absolute; + width: 45px; + height: 45px; + top: 30px; + left: 100%; + border: 0; + border-radius: 0 10px 10px 0; + background: white; + + cursor: pointer; + &.mobile { + top: 20px; + right: 20px; + z-index: 2; + left: initial; + } + } + + .list { + flex: 1; + + margin: 30px -20px 0 -20px; + padding: 0; + list-style: none; + + overflow: auto; + } + + .listItem { + a { + display: block; + border-radius: 10px; + // margin: 0 -20px; + padding: 10px 20px; + + color: $body-color; + @include medium-text; + text-decoration: none; + + &:hover { + background-color: rgba(#00C5BD, .1); + } + } + } +} + +.overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100vh; + + background: rgba(0, 0, 0, 0.7); + + transform: scale(1); + transition: + transform 0.3s cubic-bezier(0.465, 0.183, 0.153, 0.946), + opacity 0.3s cubic-bezier(0.465, 0.183, 0.153, 0.946); + z-index: 150; + +} diff --git a/src/components/map-legend/component.js b/src/components/map-legend/component.js new file mode 100644 index 000000000..fddb4062e --- /dev/null +++ b/src/components/map-legend/component.js @@ -0,0 +1,20 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import LegendItem from './legend-item'; + +const Legend = ({ layers }) => ( + + {layers.map(layer => )} + +); + + +Legend.propTypes = { + layers: PropTypes.arrayOf(PropTypes.shape({})) +}; + +Legend.defaultProps = { + layers: [] +}; + +export default Legend; diff --git a/src/components/map-legend/index.js b/src/components/map-legend/index.js new file mode 100644 index 000000000..1fbd97c99 --- /dev/null +++ b/src/components/map-legend/index.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import { activeLayersForLegend } from 'modules/layers/selectors'; +import { toggleCollapse } from 'modules/layers/actions'; +import Component from './component'; + +const mapStateToProps = state => ({ + layers: activeLayersForLegend(state), + isCollapsed: state.layers.isCollapsed +}); + +const mapDispatchToProps = { + toggleCollapse +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/map-legend/legend-item/component.js b/src/components/map-legend/legend-item/component.js new file mode 100644 index 000000000..279f709c4 --- /dev/null +++ b/src/components/map-legend/legend-item/component.js @@ -0,0 +1,35 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faTimes } from '@fortawesome/free-solid-svg-icons'; +import classnames from 'classnames'; +import styles from './style.module.scss'; + +const LegendItem = ({ id, name, toggleActive, isCollapsed, mapView }) => { + const onClickHandler = () => toggleActive({ id, isActive: false }); + + return ( +
+

{name}

+ +
+ ); +}; + +LegendItem.propTypes = { + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + toggleActive: PropTypes.func, + isCollapsed: PropTypes.bool.isRequired, + mapView: PropTypes.bool.isRequired +}; + +LegendItem.defaultProps = { + toggleActive: () => null +}; + +export default LegendItem; diff --git a/src/components/map-legend/legend-item/index.js b/src/components/map-legend/legend-item/index.js new file mode 100644 index 000000000..04b833f19 --- /dev/null +++ b/src/components/map-legend/legend-item/index.js @@ -0,0 +1,14 @@ +import { connect } from 'react-redux'; +import { toggleActive } from 'modules/layers/actions'; +import Component from './component'; + +const mapStateToProps = state => ({ + isCollapsed: state.layers.isCollapsed, + mapView: state.app.mobile.mapView +}); + +const mapDispatchToProps = { + toggleActive +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/map-legend/legend-item/stories.jsx b/src/components/map-legend/legend-item/stories.jsx new file mode 100644 index 000000000..c248f19af --- /dev/null +++ b/src/components/map-legend/legend-item/stories.jsx @@ -0,0 +1,16 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withKnobs, text, number } from '@storybook/addon-knobs'; + +import Component from './component'; + + +storiesOf('Legend item', module) + .addDecorator(withKnobs) + .add('Active', () => ( + + )); diff --git a/src/components/map-legend/legend-item/style.module.scss b/src/components/map-legend/legend-item/style.module.scss new file mode 100644 index 000000000..724443a46 --- /dev/null +++ b/src/components/map-legend/legend-item/style.module.scss @@ -0,0 +1,30 @@ +@import 'styles/vars'; + +.legendItem { + display: flex; + align-items: center; + justify-content: space-between; + padding: 5px 20px; + margin-bottom: 5px; + background: white; + border-radius: 10px; + box-shadow: 0 4px 12px 0 rgba(168,168,168,0.25); + + &.collapse { + display: none; + } + > h3 { + @include upper-text; + } +} + +.removeButton { + display: block; + border: 0; + padding: 10px; + margin: 0; + + &:hover { + cursor: pointer; + } +} diff --git a/src/components/map-legend/mobile/component.js b/src/components/map-legend/mobile/component.js new file mode 100644 index 000000000..dc3c22a2d --- /dev/null +++ b/src/components/map-legend/mobile/component.js @@ -0,0 +1,47 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import classnames from 'classnames'; +import MediaQuery from 'react-responsive'; +import { breakpoints } from 'utils/responsive'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faChevronDown, faChevronUp } from '@fortawesome/free-solid-svg-icons'; +import styles from './style.module.scss'; + +const MobileLegendControl = ({ isCollapsed, toggleCollapse }) => { + const onToggleCollapsed = () => { + toggleCollapse(!isCollapsed); + }; + + return ( + + +
+
+ Layers +
+ +
+
+
+ ); +}; + +MobileLegendControl.propTypes = { + isCollapsed: PropTypes.bool.isRequired, + toggleCollapse: PropTypes.func.isRequired +}; + +export default MobileLegendControl; diff --git a/src/components/map-legend/mobile/index.js b/src/components/map-legend/mobile/index.js new file mode 100644 index 000000000..09ef5c604 --- /dev/null +++ b/src/components/map-legend/mobile/index.js @@ -0,0 +1,13 @@ +import { connect } from 'react-redux'; +import { toggleCollapse } from 'modules/layers/actions'; +import Component from './component'; + +const mapStateToProps = state => ({ + isCollapsed: state.layers.isCollapsed +}); + +const mapDispatchToProps = { + toggleCollapse +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/map-legend/mobile/style.module.scss b/src/components/map-legend/mobile/style.module.scss new file mode 100644 index 000000000..55b80f0d0 --- /dev/null +++ b/src/components/map-legend/mobile/style.module.scss @@ -0,0 +1,39 @@ +@import 'styles/vars'; + +.layersCollapse { + display: flex; + justify-content: flex-end; + &.collapse { + width: 100%; + } + + > * { + padding: 0 20px 5px 20px; + margin-bottom: 5px; + background: white; + border-radius: 10px; + box-shadow: 0 4px 12px 0 rgba(168,168,168,0.25); + align-items: center; + } + > div { + width: 100%; + } + .title { + color: rgba(0, 0, 0, 0.85); + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + line-height: 15px; + display: none; + + &.collapse { + display: flex; + } + } + .layersBtn { + width: 15px; + height: 45px; + box-sizing: content-box; + margin-left: 5px; + } +} diff --git a/src/components/map-legend/mobile/style.module.scss~HEAD b/src/components/map-legend/mobile/style.module.scss~HEAD new file mode 100644 index 000000000..7c415abd0 --- /dev/null +++ b/src/components/map-legend/mobile/style.module.scss~HEAD @@ -0,0 +1,39 @@ +@import 'styles/vars'; + +.layersCollapse { + display: flex; + justify-content: flex-end; + &.collapse { + width: 100%; + } + + > * { + padding: 0 20px 5px 20px; + margin-bottom: 5px; + background: white; + border-radius: 10px; + box-shadow: 0 4px 12px 0 rgba(168,168,168,0.25); + align-items: center; + } + > div { + width: 100%; + } + .title { + color: rgba(0, 0, 0, 0.85); + font-size: 12px; + font-weight: bold; + letter-spacing: 1px; + line-height: 15px; + display: none; + + &.collapse { + display: flex; + } + } + .layersBtn { + width: 15px; + height: 45px; + box-sizing: content-box; + margin-right: 5px; + } +} diff --git a/src/components/map/component.js b/src/components/map/component.js new file mode 100644 index 000000000..af421af77 --- /dev/null +++ b/src/components/map/component.js @@ -0,0 +1,98 @@ +import React, { PureComponent } from 'react'; +import MapGL, { NavigationControl } from 'react-map-gl'; +import PropTypes from 'prop-types'; +import MediaQuery from 'react-responsive'; +import MobileLegendControl from 'components/map-legend/mobile'; +import classnames from 'classnames'; +import { breakpoints } from 'utils/responsive'; +import BasemapSelector from 'components/basemap-selector'; +import Legend from 'components/map-legend'; +import styles from './style.module.scss'; + +class Map extends PureComponent { + static propTypes = { + basemap: PropTypes.string, + viewport: PropTypes.shape({}), + setViewport: PropTypes.func, + isCollapse: PropTypes.bool.isRequired + } + + static defaultProps = { + basemap: 'light', + viewport: { + width: window.innerWidth, + height: window.innerHeight, + longitude: 0, + latitude: 0, + zoom: 2, + maxZoom: 16, + bearing: 0, + pitch: 0 + }, + setViewport: () => { } + } + + componentDidMount() { + window.addEventListener('resize', this.resize); + this.resize(); + } + + componentWillUnmount() { + window.removeEventListener('resize', this.resize); + } + + onViewportChange = (viewport) => { + const { setViewport } = this.props; + setViewport(viewport); + } + + resize = () => { + const { viewport } = this.props; + this.onViewportChange({ + ...viewport, + width: window.innerWidth, + height: window.innerHeight + }); + } + + render() { + const { + mapboxApiAccessToken, + mapStyle, + viewport, + isCollapse + } = this.props; + + return ( + +
+ + + +
+
+ + + +
+ + +
+
+
+ ); + } +} + +export default Map; diff --git a/src/components/map/index.js b/src/components/map/index.js new file mode 100644 index 000000000..73843d4bb --- /dev/null +++ b/src/components/map/index.js @@ -0,0 +1,18 @@ +import { connect } from 'react-redux'; +import { setViewport } from 'modules/map/actions'; +import { mapStyle } from 'modules/map-styles/selectors'; + +import Component from './component'; + +const mapStateToProps = state => ({ + ...state.map, + mapStyle: mapStyle(state), + mapboxApiAccessToken: process.env.REACT_APP_MAPBOX_ACCESS_TOKEN, + isCollapse: state.layers.isCollapsed +}); + +const mapDispatchToProps = { + setViewport +}; + +export default connect(mapStateToProps, mapDispatchToProps)(Component); diff --git a/src/components/map/stories.jsx b/src/components/map/stories.jsx new file mode 100644 index 000000000..4f111abaf --- /dev/null +++ b/src/components/map/stories.jsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withProvider } from 'utils/storybookProvider'; +import Map from './component'; + +storiesOf('Map', module) + .addDecorator(withProvider) + .add('map', () => ( +
+ +
+ )); diff --git a/src/components/map/style.module.scss b/src/components/map/style.module.scss new file mode 100644 index 000000000..b82efb7d3 --- /dev/null +++ b/src/components/map/style.module.scss @@ -0,0 +1,44 @@ +@import 'styles/vars'; + +$space-around-map: 30px; +$space-top-map-mobile: 5px; +$space-left-map-mobile: 20px; + +.map { + background: white; +} + +.navigation { + position: absolute; + right: $space-around-map; + top: $space-around-map; +} + +.legend { + position: absolute; + right: $space-around-map; + bottom: $space-around-map; + @media screen and (max-width: map-get($breakpoints, md)) { + top: $space-top-map-mobile; + left: $space-left-map-mobile; + &.expanded { + display: flex; + flex-direction: row-reverse; + bottom: inherit; + right: 25px; + } + } + .tooltip { + display: flex; + flex-direction: column; + } +} + +// Mapbox override +:global(.mapboxgl-ctrl-group:not(:empty)) { + box-shadow: 0 4px 12px 0 rgba(168, 168, 168, 0.25); +} + +:global(.mapboxgl-ctrl-group > button) { + background: white; +} diff --git a/src/components/modal/component.js b/src/components/modal/component.js new file mode 100644 index 000000000..0bfa18f37 --- /dev/null +++ b/src/components/modal/component.js @@ -0,0 +1,24 @@ +import React from 'react'; +import Modal from 'react-modal'; +import './styles.scss'; + +const customStyles = { + overlay: { + background: 'rgba(0, 0, 0, 0.7)' + } +}; + +Modal.setAppElement('#root'); + +export default (props) => { + const { children, ...domProps } = props; + + return ( + + {children} + + ); +}; diff --git a/src/components/modal/index.js b/src/components/modal/index.js new file mode 100644 index 000000000..f1d269317 --- /dev/null +++ b/src/components/modal/index.js @@ -0,0 +1,3 @@ +import Component from './component'; + +export default Component; diff --git a/src/components/modal/stories.jsx b/src/components/modal/stories.jsx new file mode 100644 index 000000000..3d020d790 --- /dev/null +++ b/src/components/modal/stories.jsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withProvider } from 'utils/storybookProvider'; +import Modal from './component'; + +storiesOf('Modal', module) + .addDecorator(withProvider) + .add('Open', () => ( + + )); diff --git a/src/components/modal/styles.scss b/src/components/modal/styles.scss new file mode 100644 index 000000000..859ab03a6 --- /dev/null +++ b/src/components/modal/styles.scss @@ -0,0 +1,19 @@ +.ReactModal__Overlay { + z-index: 101; + background: rgba(0, 0, 0, 0.7); + transform: scale(1.15); + transition: + transform 0.1s cubic-bezier(0.465, 0.183, 0.153, 0.946), + opacity 0.1s cubic-bezier(0.465, 0.183, 0.153, 0.946); +} + +.ReactModal__Overlay--after-open { + transform: scale(1); + transition: + transform 0.3s cubic-bezier(0.465, 0.183, 0.153, 0.946), + opacity 0.3s cubic-bezier(0.465, 0.183, 0.153, 0.946); +} + +.ReactModal__Overlay--before-close { + transform: scale(1.15); +} diff --git a/src/components/pages/component.js b/src/components/pages/component.js new file mode 100644 index 000000000..3d33ee994 --- /dev/null +++ b/src/components/pages/component.js @@ -0,0 +1,29 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +// todo: add Universal component or loadable +import AppPage from 'pages/app'; +import NotFoundPage from 'pages/not-found'; + +const pageMap = new Map([ + ['PAGE/APP', AppPage], + ['PAGE/COUNTRY', AppPage], + ['PAGE/AOI', AppPage], + ['PAGE/WDPA', AppPage] +]); + +// prompts or error logging should be handled here +const Pages = ({ page: { current, payload } }) => { + const Page = pageMap.has(current) ? pageMap.get(current) : NotFoundPage; + + return ; +}; + +Pages.propTypes = { + page: PropTypes.shape({ + current: PropTypes.string.isRequired, + payload: PropTypes.shape({}).isRequired + }).isRequired +}; + +export default Pages; diff --git a/src/components/pages/index.js b/src/components/pages/index.js new file mode 100644 index 000000000..5e3725362 --- /dev/null +++ b/src/components/pages/index.js @@ -0,0 +1,6 @@ +import { connect } from 'react-redux'; +import Component from './component'; + +export default connect( + ({ page }) => ({ page }) +)(Component); diff --git a/src/components/select/component.js b/src/components/select/component.js new file mode 100644 index 000000000..5a3910971 --- /dev/null +++ b/src/components/select/component.js @@ -0,0 +1,56 @@ +import React, { PureComponent } from 'react'; +import ReactSelect from 'react-select'; +import PropTypes from 'prop-types'; + +import { styles, theme } from './style'; + +class Select extends PureComponent { + static propTypes = { + options: PropTypes.arrayOf(PropTypes.shape({})).isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + onChange: PropTypes.func + }; + + static defaultProps = { + value: null, + onChange: () => null + } + + state = { selectedOption: null } + + options = { + isSearchable: false, + theme, + styles + } + + constructor(props) { + super(props); + this.state = { selectedOption: props.value }; + } + + handleChange = (selectedOption) => { + const { onChange } = this.props; + this.setState({ selectedOption }); + onChange(selectedOption.value); + } + + render() { + const { value: defaultValue, options, onChange, ...props } = this.props; + const { selectedOption } = this.state; + const selectedValue = options.find(opt => opt.value === selectedOption); + + return ( + + ); + } +} + +export default Select; diff --git a/src/components/select/index.js b/src/components/select/index.js new file mode 100644 index 000000000..c2ca7c046 --- /dev/null +++ b/src/components/select/index.js @@ -0,0 +1,3 @@ +import SelectComponent from './component'; + +export default SelectComponent; diff --git a/src/components/select/stories.jsx b/src/components/select/stories.jsx new file mode 100644 index 000000000..561325bcf --- /dev/null +++ b/src/components/select/stories.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { withKnobs, object } from '@storybook/addon-knobs'; + +import Select from './component'; + +const options = [ + { label: '1996', value: '1996' }, + { label: '2007', value: '2007' }, + { label: '2008', value: '2008' }, + { label: '2009', value: '2009' }, + { label: '2010', value: '2010' }, + { label: '2015', value: '2015' }, + { label: '2016', value: '2016' } +]; + +const defaultValue = { label: '2016', value: '2016' }; + +storiesOf('Select', module) + .addDecorator(withKnobs) + .add('Selector', () => ( +