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 000000000..ae57893aa Binary files /dev/null and b/public/android-chrome-192x192.png differ diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 000000000..fa1c7974d Binary files /dev/null and b/public/android-chrome-512x512.png differ diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 000000000..40b7740da Binary files /dev/null and b/public/apple-touch-icon.png differ 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 000000000..815384b33 Binary files /dev/null and b/public/favicon-16x16.png differ diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 000000000..709a7f301 Binary files /dev/null and b/public/favicon-32x32.png differ diff --git a/public/favicon.ico b/public/favicon.ico index a11777cc4..f17b0646c 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ 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 000000000..976d88ea5 Binary files /dev/null and b/public/mstile-150x150.png differ diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg new file mode 100644 index 000000000..3dba2ab76 --- /dev/null +++ b/public/safari-pinned-tab.svg @@ -0,0 +1,30 @@ + + + + +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 ( -
-
- logo -

- Edit src/App.js and save to reload. -

- - Learn React - -
-
- ); -} - -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 000000000..6626d2c99 Binary files /dev/null and b/src/components/basemap-selector/thumbs/btn-dark@2x.png differ diff --git a/src/components/basemap-selector/thumbs/btn-light@2x.png b/src/components/basemap-selector/thumbs/btn-light@2x.png new file mode 100755 index 000000000..d34d5e0ff Binary files /dev/null and b/src/components/basemap-selector/thumbs/btn-light@2x.png differ 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 000000000..942c21605 Binary files /dev/null and b/src/components/basemap-selector/thumbs/btn-satellite@2x.png differ diff --git a/src/components/basemap-selector/thumbs/dark.png b/src/components/basemap-selector/thumbs/dark.png new file mode 100644 index 000000000..500bfc65b Binary files /dev/null and b/src/components/basemap-selector/thumbs/dark.png differ diff --git a/src/components/basemap-selector/thumbs/light.png b/src/components/basemap-selector/thumbs/light.png new file mode 100644 index 000000000..1d553aab6 Binary files /dev/null and b/src/components/basemap-selector/thumbs/light.png differ diff --git a/src/components/basemap-selector/thumbs/satellite.jpeg b/src/components/basemap-selector/thumbs/satellite.jpeg new file mode 100644 index 000000000..33c62c73b Binary files /dev/null and b/src/components/basemap-selector/thumbs/satellite.jpeg differ diff --git a/src/components/button/component.js b/src/components/button/component.js new file mode 100644 index 000000000..be255c03d --- /dev/null +++ b/src/components/button/component.js @@ -0,0 +1,30 @@ +import React from 'react'; +import classnames from 'classnames'; +import styles from './style.module.scss'; + +export default (props) => { + 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', () => ( +