diff --git a/app/javascript/assets/icons/blog.svg b/app/javascript/assets/icons/blog.svg new file mode 100644 index 0000000000..590bf7fd7f --- /dev/null +++ b/app/javascript/assets/icons/blog.svg @@ -0,0 +1,5 @@ + + +blog + + diff --git a/app/javascript/assets/icons/contribute.svg b/app/javascript/assets/icons/contribute.svg new file mode 100644 index 0000000000..9e585b04b8 --- /dev/null +++ b/app/javascript/assets/icons/contribute.svg @@ -0,0 +1,5 @@ + + +contribute + + diff --git a/app/javascript/assets/icons/developer.svg b/app/javascript/assets/icons/developer.svg new file mode 100644 index 0000000000..f6045ac1f8 --- /dev/null +++ b/app/javascript/assets/icons/developer.svg @@ -0,0 +1,5 @@ + + +Developer + + diff --git a/app/javascript/assets/icons/forum.svg b/app/javascript/assets/icons/forum.svg new file mode 100644 index 0000000000..b20e6dc683 --- /dev/null +++ b/app/javascript/assets/icons/forum.svg @@ -0,0 +1,5 @@ + + +forum + + diff --git a/app/javascript/assets/icons/howto.svg b/app/javascript/assets/icons/howto.svg new file mode 100644 index 0000000000..a0e4caa047 --- /dev/null +++ b/app/javascript/assets/icons/howto.svg @@ -0,0 +1,5 @@ + + +howto + + diff --git a/app/javascript/assets/icons/menu.svg b/app/javascript/assets/icons/menu.svg new file mode 100644 index 0000000000..2e7aa3e5f0 --- /dev/null +++ b/app/javascript/assets/icons/menu.svg @@ -0,0 +1,6 @@ + +menu + + + + diff --git a/app/javascript/assets/icons/more.svg b/app/javascript/assets/icons/more.svg new file mode 100755 index 0000000000..e2ac56fa75 --- /dev/null +++ b/app/javascript/assets/icons/more.svg @@ -0,0 +1,7 @@ + + +more + + + + diff --git a/app/javascript/assets/icons/mygfw.svg b/app/javascript/assets/icons/mygfw.svg new file mode 100755 index 0000000000..c37649171b --- /dev/null +++ b/app/javascript/assets/icons/mygfw.svg @@ -0,0 +1,5 @@ + + +mygfw + + diff --git a/app/javascript/assets/icons/open-data.svg b/app/javascript/assets/icons/open-data.svg new file mode 100644 index 0000000000..84ee9de036 --- /dev/null +++ b/app/javascript/assets/icons/open-data.svg @@ -0,0 +1,6 @@ + + +open data + + + diff --git a/app/javascript/assets/icons/sgf.svg b/app/javascript/assets/icons/sgf.svg new file mode 100644 index 0000000000..32d70531b4 --- /dev/null +++ b/app/javascript/assets/icons/sgf.svg @@ -0,0 +1,5 @@ + + +SGF + + diff --git a/app/javascript/assets/icons/stories.svg b/app/javascript/assets/icons/stories.svg new file mode 100644 index 0000000000..8945e14b84 --- /dev/null +++ b/app/javascript/assets/icons/stories.svg @@ -0,0 +1,5 @@ + + +stories + + diff --git a/app/javascript/assets/logos/gfw-climate.png b/app/javascript/assets/logos/gfw-climate.png new file mode 100644 index 0000000000..59b9b0d2c9 Binary files /dev/null and b/app/javascript/assets/logos/gfw-climate.png differ diff --git a/app/javascript/assets/logos/gfw-commodities.png b/app/javascript/assets/logos/gfw-commodities.png new file mode 100644 index 0000000000..841779ddce Binary files /dev/null and b/app/javascript/assets/logos/gfw-commodities.png differ diff --git a/app/javascript/assets/logos/gfw-fires.png b/app/javascript/assets/logos/gfw-fires.png new file mode 100644 index 0000000000..659c9a80f8 Binary files /dev/null and b/app/javascript/assets/logos/gfw-fires.png differ diff --git a/app/javascript/assets/logos/gfw-watcher.png b/app/javascript/assets/logos/gfw-watcher.png new file mode 100644 index 0000000000..72e8f0158a Binary files /dev/null and b/app/javascript/assets/logos/gfw-watcher.png differ diff --git a/app/javascript/assets/logos/gfw-water.png b/app/javascript/assets/logos/gfw-water.png new file mode 100644 index 0000000000..afdc55d732 Binary files /dev/null and b/app/javascript/assets/logos/gfw-water.png differ diff --git a/app/javascript/components/header/component.jsx b/app/javascript/components/header/component.jsx new file mode 100644 index 0000000000..3ff7b8de61 --- /dev/null +++ b/app/javascript/components/header/component.jsx @@ -0,0 +1,215 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import { SCREEN_L } from 'utils/constants'; +import cx from 'classnames'; + +import { NavLink } from 'redux-first-router-link'; + +import Icon from 'components/ui/icon'; + +import gfwLogo from 'assets/logos/gfw.png'; +import moreIcon from 'assets/icons/more.svg'; +import menuIcon from 'assets/icons/menu.svg'; +import myGfwIcon from 'assets/icons/mygfw.svg'; +import closeIcon from 'assets/icons/close.svg'; +import arrowIcon from 'assets/icons/arrow-down.svg'; + +import MyGFWLogin from './components/mygfw-login'; +import DropdownMenu from './components/dropdown-menu'; +import SubmenuPanel from './components/submenu-panel'; + +import './styles.scss'; + +class Header extends PureComponent { + render() { + const { + className, + setShowPanel, + setShowMyGfw, + setShowLangSelector, + showPanel, + showMyGfw, + showLangSelector, + handleLangSelect, + showHeader, + toggleMenu, + languages, + myGfwLinks, + activeLang, + navMain, + apps, + moreLinks, + fullScreen, + loggedIn + } = this.props; + const isMobile = window.innerWidth < SCREEN_L; + let moreText = fullScreen ? 'close' : 'more'; + if (!fullScreen && isMobile) { + moreText = 'menu'; + } + let moreMenuIcon = fullScreen || showPanel ? closeIcon : moreIcon; + let moreMenuClassName = + fullScreen || showPanel ? 'icon-close' : 'icon-more'; + if (isMobile && !fullScreen && !showPanel) { + moreMenuIcon = menuIcon; + moreMenuClassName = 'icon-menu'; + } + + return ( + (!fullScreen || (fullScreen && showHeader)) && ( +
+
+
+ {fullScreen ? ( + + ) : ( + + Global Forest Watch + + )} +
+ {!isMobile && ( +
    { + setShowMyGfw(false); + setShowLangSelector(false); + }} + role="button" // eslint-disable-line + > + {navMain.map(item => ( +
  • + + {item.label} + +
  • + ))} +
+ )} +
    + {!isMobile && ( +
  • + + {showLangSelector && ( + + )} +
  • + )} + {!isMobile && ( +
  • + + {showMyGfw && + loggedIn && ( + + )} + {showMyGfw && + !loggedIn && } +
  • + )} +
  • + +
  • +
+
+
+
+ {showPanel && ( + { + setShowMyGfw(false); + setShowLangSelector(false); + }} + setShowPanel={setShowPanel} + handleLangSelect={handleLangSelect} + /> + )} +
+ ) + ); + } +} + +Header.propTypes = { + className: PropTypes.string, + setShowPanel: PropTypes.func, + setShowMyGfw: PropTypes.func, + setShowLangSelector: PropTypes.func, + showPanel: PropTypes.bool, + showMyGfw: PropTypes.bool, + showLangSelector: PropTypes.bool, + handleLangSelect: PropTypes.func, + languages: PropTypes.array, + activeLang: PropTypes.object, + myGfwLinks: PropTypes.array, + navMain: PropTypes.array, + apps: PropTypes.array, + moreLinks: PropTypes.array, + fullScreen: PropTypes.bool, + showHeader: PropTypes.bool, + toggleMenu: PropTypes.func, + loggedIn: PropTypes.bool +}; + +export default Header; diff --git a/app/javascript/components/header/components/dropdown-menu/component.jsx b/app/javascript/components/header/components/dropdown-menu/component.jsx new file mode 100644 index 0000000000..91e4f4b005 --- /dev/null +++ b/app/javascript/components/header/components/dropdown-menu/component.jsx @@ -0,0 +1,48 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import { NavLink } from 'redux-first-router-link'; + +import './styles.scss'; + +class DropdownMenu extends PureComponent { + render() { + const { className, options, handleSelect, selected } = this.props; + + return ( + + ); + } +} + +DropdownMenu.propTypes = { + className: PropTypes.string, + options: PropTypes.array, + handleSelect: PropTypes.func, + selected: PropTypes.object +}; + +export default DropdownMenu; diff --git a/app/javascript/components/header/components/dropdown-menu/index.js b/app/javascript/components/header/components/dropdown-menu/index.js new file mode 100644 index 0000000000..f1d269317e --- /dev/null +++ b/app/javascript/components/header/components/dropdown-menu/index.js @@ -0,0 +1,3 @@ +import Component from './component'; + +export default Component; diff --git a/app/javascript/components/header/components/dropdown-menu/styles.scss b/app/javascript/components/header/components/dropdown-menu/styles.scss new file mode 100644 index 0000000000..bf77e61cc8 --- /dev/null +++ b/app/javascript/components/header/components/dropdown-menu/styles.scss @@ -0,0 +1,43 @@ +@import '~styles/settings.scss'; + +.c-dropdown-menu { + background-color: $white; + width: rem(200px); + padding: rem(15px) rem(20px); + border: solid 1px $border; + + &.-plain { + border: 0; + padding: 0; + width: 100%; + } + + > li { + > button, + a { + display: block; + text-transform: uppercase; + font-size: rem(14px); + padding: rem(15px) 0; + width: 100%; + color: $slate; + cursor: pointer; + text-align: left; + + &:hover { + color: darken($slate, 30%); + } + } + + &.active { + > button, + a { + color: $green-gfw; + + &:hover { + color: darken($green-gfw, 10%); + } + } + } + } +} diff --git a/app/javascript/components/header/components/mygfw-login/component.jsx b/app/javascript/components/header/components/mygfw-login/component.jsx new file mode 100644 index 0000000000..550e860063 --- /dev/null +++ b/app/javascript/components/header/components/mygfw-login/component.jsx @@ -0,0 +1,53 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import './styles.scss'; + +const AUTH_URL = `${process.env.GFW_API}/auth`; + +const socialButtons = [ + { + label: 'Twitter', + value: 'twitter' + }, + { + label: 'Facebook', + value: 'facebook' + }, + { + label: 'Google', + value: 'google' + } +]; + +class MyGFWLogin extends PureComponent { + render() { + const { className, plain } = this.props; + + return ( +
+

+ Log in is required so you can view, manage, and delete your + subscriptions. Questions? Contact us +

+ {socialButtons.map(s => ( + + Login with {s.label} + + ))} +
+ ); + } +} + +MyGFWLogin.propTypes = { + className: PropTypes.string, + plain: PropTypes.bool +}; + +export default MyGFWLogin; diff --git a/app/javascript/components/header/components/mygfw-login/index.js b/app/javascript/components/header/components/mygfw-login/index.js new file mode 100644 index 0000000000..f1d269317e --- /dev/null +++ b/app/javascript/components/header/components/mygfw-login/index.js @@ -0,0 +1,3 @@ +import Component from './component'; + +export default Component; diff --git a/app/javascript/components/header/components/mygfw-login/styles.scss b/app/javascript/components/header/components/mygfw-login/styles.scss new file mode 100644 index 0000000000..b72aa02540 --- /dev/null +++ b/app/javascript/components/header/components/mygfw-login/styles.scss @@ -0,0 +1,58 @@ +@import '~styles/settings.scss'; + +.c-my-gfw { + background-color: $white; + width: rem(320px); + padding: rem(25px) rem(30px) rem(30px); + border: solid 1px $border; + + &.-plain { + border: 0; + width: 100%; + padding: 0; + } + + p { + font-size: rem(14px); + margin-bottom: rem(20px); + } + + .social-btn { + border-radius: 0; + height: rem(40px); + width: 100%; + display: flex; + align-items: center; + justify-content: center; + color: $white; + text-transform: uppercase; + font-size: rem(12px); + font-weight: 500; + + &.-twitter { + background-color: $twitter; + margin-bottom: rem(10px); + + &:hover { + background-color: darken($twitter, 10%); + } + } + + &.-facebook { + background-color: $facebook; + margin-bottom: rem(10px); + + &:hover { + background-color: darken($facebook, 10%); + } + } + + &.-google { + background-color: $googleplus; + + &:hover { + background-color: darken($googleplus, 10%); + } + } + } +} diff --git a/app/javascript/components/header/components/submenu-panel/component.jsx b/app/javascript/components/header/components/submenu-panel/component.jsx new file mode 100644 index 0000000000..30c4834aed --- /dev/null +++ b/app/javascript/components/header/components/submenu-panel/component.jsx @@ -0,0 +1,178 @@ +import React, { PureComponent } from 'react'; +import PropTypes from 'prop-types'; +import cx from 'classnames'; + +import Icon from 'components/ui/icon'; +import Search from 'components/ui/search'; + +import moreIcon from 'assets/icons/more.svg'; + +import DropdownMenu from '../dropdown-menu'; +import MyGfwLogin from '../mygfw-login'; + +import './styles.scss'; + +class Header extends PureComponent { + state = { + search: '' + }; + + handleSubmit = () => { + window.location.href = `${window.location.origin}/search?query=${ + this.state.search + }`; + }; + + handleSearchChange = search => { + this.setState({ search }); + }; + + render() { + const { + className, + apps, + moreLinks, + fullScreen, + onClick, + isMobile, + navMain, + activeLang, + languages, + myGfwLinks, + setShowPanel, + handleLangSelect, + toggleMenu, + loggedIn + } = this.props; + + return ( +
+
+
+ + {isMobile && ( +
+ +
+ )} + {isMobile && ( +
+

Select a language

+ { + handleLangSelect(lang); + if (fullScreen) { + toggleMenu(); + } else { + setShowPanel(false); + } + }} + /> +
+ )} + {isMobile && ( +
+

My GFW

+ {loggedIn ? ( + + ) : ( + + )} +
+ )} +
+

Other applications

+
+ {apps && + apps.map(d => ( + + +
+
+

More in GFW

+ +
+ + Sitemap + +
+
+
+ ); + } +} + +Header.propTypes = { + className: PropTypes.string, + apps: PropTypes.array, + moreLinks: PropTypes.array, + fullScreen: PropTypes.bool, + onClick: PropTypes.func, + isMobile: PropTypes.bool, + navMain: PropTypes.array, + activeLang: PropTypes.object, + languages: PropTypes.array, + myGfwLinks: PropTypes.array, + setShowPanel: PropTypes.func, + handleLangSelect: PropTypes.func, + toggleMenu: PropTypes.func, + loggedIn: PropTypes.bool +}; + +export default Header; diff --git a/app/javascript/components/header/components/submenu-panel/index.js b/app/javascript/components/header/components/submenu-panel/index.js new file mode 100644 index 0000000000..f1d269317e --- /dev/null +++ b/app/javascript/components/header/components/submenu-panel/index.js @@ -0,0 +1,3 @@ +import Component from './component'; + +export default Component; diff --git a/app/javascript/components/header/components/submenu-panel/styles.scss b/app/javascript/components/header/components/submenu-panel/styles.scss new file mode 100644 index 0000000000..5c1b474ebf --- /dev/null +++ b/app/javascript/components/header/components/submenu-panel/styles.scss @@ -0,0 +1,102 @@ +@import '~styles/settings.scss'; + +.c-submenu-panel { + background-color: $white; + padding: rem(40px) 0; + + &:focus { + outline: none; + } + + &.-full-screen { + min-height: calc(100vh - 76px); + } + + .menu-search { + margin-bottom: rem(40px); + } + + h4, + .title { + text-transform: uppercase; + color: $slate; + font-size: rem(14px); + margin-bottom: rem(23px); + } + + .menu-section { + padding-bottom: rem(30px); + margin-bottom: rem(30px); + border-bottom: solid 1px $border; + + .apps-slider { + overflow-x: scroll; + display: flex; + margin-left: rem(-15px); + width: calc(100% + 30px); + padding: 0 rem(15px); + } + + .app-card { + margin-right: rem(15px); + + .app-image { + width: rem(90px); + height: rem(90px); + background-position: center; + background-repeat: no-repeat; + background-size: contain; + } + + .all-apps { + border: solid 1px $border; + padding: rem(10px); + display: flex; + flex-direction: column; + align-items: center; + justify-content: flex-end; + font-size: rem(12px); + text-transform: uppercase; + text-align: center; + font-weight: 400; + color: $slate; + width: rem(90px); + height: rem(90px); + + .icon-more { + width: rem(25px); + height: rem(25px); + margin-bottom: rem(5px); + } + } + } + + .more-links { + > li { + margin-bottom: rem(20px); + + a { + display: flex; + align-items: center; + font-size: rem(12px); + color: $slate; + text-transform: uppercase; + + svg { + margin-right: rem(5px); + } + + &:hover { + color: darken($slate, 30%); + } + } + } + } + } + + &.-mobile { + .menu-search { + margin-bottom: rem(30px); + } + } +} diff --git a/app/javascript/components/header/config.js b/app/javascript/components/header/config.js new file mode 100644 index 0000000000..020772a391 --- /dev/null +++ b/app/javascript/components/header/config.js @@ -0,0 +1,139 @@ +import axios from 'axios'; + +import gfwClimate from 'assets/logos/gfw-climate.png'; +import gfwFires from 'assets/logos/gfw-fires.png'; +import gfwCommodities from 'assets/logos/gfw-commodities.png'; +import gfwWater from 'assets/logos/gfw-water.png'; +import forestWatcher from 'assets/logos/gfw-watcher.png'; + +import developer from 'assets/icons/developer.svg'; +import howto from 'assets/icons/howto.svg'; +import sgf from 'assets/icons/sgf.svg'; +import openData from 'assets/icons/open-data.svg'; +import contribute from 'assets/icons/contribute.svg'; +import blog from 'assets/icons/blog.svg'; +import stories from 'assets/icons/stories.svg'; +import forum from 'assets/icons/forum.svg'; + +export default { + navMain: [ + { + label: 'Map', + path: '/v2/map', + navLink: true + }, + { + label: 'Dashboards', + path: '/dashboards/global' + }, + { + label: 'Blog', + path: 'http://blog.globalforestwatch.org', + target: '_blank', + rel: 'noopener nofollower' + }, + { + label: 'About', + path: '/map' + } + ], + myGfwLinks: [ + { + label: 'My subscriptions', + path: '/my_gfw/subscriptions' + }, + { + label: 'My stories', + path: '/my_gfw/stories' + }, + { + label: 'My profile', + path: '/my_gfw' + }, + { + label: 'Logout', + path: '/auth/logout', + onSelect: e => { + e.preventDefault(); + axios + .get(`${process.env.GFW_API}/auth/logout`, { withCredentials: true }) + .then(response => { + if (response.status < 400) { + window.location.reload(); + } else { + console.warn('Failed to logout'); + } + }); + } + } + ], + apps: [ + { + label: 'GFW Climate', + path: 'http://climate.globalforestwatch.org', + image: gfwClimate + }, + { + label: 'GFW Fires', + path: 'http://fires.globalforestwatch.org', + image: gfwFires + }, + { + label: 'GFW Comodities', + path: 'http://commodities.globalforestwatch.org', + image: gfwCommodities + }, + { + label: 'GFW Water', + path: 'http://water.globalforestwatch.org', + image: gfwWater + }, + { + label: 'Forest Watcher', + path: 'http://forestwatcher.globalforestwatch.org', + image: forestWatcher + } + ], + moreLinks: [ + { + label: 'Developer Tools', + path: 'http://developers.globalforestwatch.org', + icon: developer + }, + { + label: 'How to Portal', + path: 'http://www.globalforestwatch.org/howto', + icon: howto + }, + { + label: 'Small Grants Fund', + path: '/small-grants-fund', + icon: sgf + }, + { + label: 'Open data portal', + path: 'http://data.globalforestwatch.org/', + icon: openData + }, + { + label: 'Contribute data', + path: '/contribute-data', + icon: contribute + }, + { + label: 'Blog', + path: 'https://blog.globalforestwatch.org', + icon: blog + }, + { + label: 'Stories', + path: '/stories', + icon: stories + }, + { + label: 'Discussion Forum', + path: 'https://groups.google.com/forum/#!forum/globalforestwatch', + icon: forum + } + ] +}; diff --git a/app/javascript/components/header/index.js b/app/javascript/components/header/index.js new file mode 100644 index 0000000000..6bd412826c --- /dev/null +++ b/app/javascript/components/header/index.js @@ -0,0 +1,84 @@ +import { createElement, PureComponent } from 'react'; +import PropTypes from 'prop-types'; + +import config from './config'; +import Component from './component'; + +class HeaderContainer extends PureComponent { + constructor(props) { + super(props); + const { showPanel, showMyGfw, showLangSelector } = props; + + const txData = JSON.parse(localStorage.getItem('txlive:languages')); + const txLang = JSON.parse(localStorage.getItem('txlive:selectedlang')); + const languages = + txData && + txData.source && + [txData.source].concat(txData.translation).map(l => ({ + label: l.name, + value: l.code + })); + + this.state = { + showPanel, + showMyGfw, + showLangSelector, + showHeader: false, + languages, + lang: txLang + }; + } + + setShowPanel = showPanel => { + this.setState({ + showPanel, + showMyGfw: false, + showLangSelector: false + }); + }; + + setShowMyGfw = showMyGfw => { + this.setState({ + showMyGfw, + showPanel: false, + showLangSelector: false + }); + }; + + setShowLangSelector = showLangSelector => { + this.setState({ + showLangSelector, + showPanel: false, + showMyGfw: false + }); + }; + + handleLangSelect = lang => { + localStorage.setItem('txlive:selectedlang', `"${lang}"`); + this.setState({ lang }); + this.setShowLangSelector(false); + }; + + render() { + const { languages, lang } = this.state; + const activeLang = languages && languages.find(l => l.value === lang); + return createElement(Component, { + ...this.state, + ...this.props, + setShowPanel: this.setShowPanel, + setShowMyGfw: this.setShowMyGfw, + setShowLangSelector: this.setShowLangSelector, + handleLangSelect: this.handleLangSelect, + activeLang, + ...config + }); + } +} + +HeaderContainer.propTypes = { + showPanel: PropTypes.bool, + showMyGfw: PropTypes.bool, + showLangSelector: PropTypes.bool +}; + +export default HeaderContainer; diff --git a/app/javascript/components/header/styles.scss b/app/javascript/components/header/styles.scss new file mode 100644 index 0000000000..5dabbe2379 --- /dev/null +++ b/app/javascript/components/header/styles.scss @@ -0,0 +1,167 @@ +@import '~styles/settings.scss'; + +$header-height: rem(55px); +$full-screen-height: rem(76px); + +.c-header { + background: $white; + height: $header-height; + + .nav-menu { + border-bottom: solid 1px $border; + position: relative; + + > div { + position: relative; + } + } + + .logo { + position: absolute; + top: 0; + z-index: 1; + cursor: pointer; + } + + .nav { + display: flex; + justify-content: space-between; + position: relative; + height: $header-height; + + li { + height: 100%; + position: relative; + font-size: rem(14px); + + .nav-link, + .menu-link { + position: relative; + font-family: $font-family-1; + color: $slate; + height: 100%; + text-transform: uppercase; + cursor: pointer; + display: flex; + align-items: center; + + &:hover { + color: darken($slate, 30%); + } + } + + .nav-link { + &::after { + content: ''; + height: rem(5px); + position: absolute; + bottom: -1px; + right: 0; + left: 0; + background-color: transparent; + } + + &.-active, + &:hover { + &::after { + background-color: $green-gfw; + } + } + } + + svg { + fill: $slate; + + &:hover { + color: darken($slate, 30%); + } + } + } + + .nav-main { + display: flex; + align-items: center; + height: 100%; + justify-content: flex-start; + padding-left: rem(100px); + flex: 1; + + > li .nav-link { + padding: 0 rem(10px); + margin: 0 rem(5px); + } + } + + .nav-alt { + display: flex; + justify-content: flex-end; + align-items: center; + height: 100%; + border-left: solid 1px $border; + padding-left: rem(10px); + + &.-mobile { + flex: 1; + } + + > li { + margin-left: rem(30px); + position: relative; + font-size: rem(12px); + + svg { + margin-top: -1px; + margin-left: rem(5px); + } + } + + .sub-menu { + position: absolute; + right: -5px; + z-index: 1; + + &::before, + &::after { + content: ''; + display: block; + position: absolute; + width: 0; + height: 0; + border-style: solid; + } + + &::before { + top: -12px; + border-color: transparent transparent $border transparent; + border-width: 6px; + right: 6px; + } + + &::after { + top: -10px; + border-color: transparent transparent $white transparent; + border-width: 5px; + right: 7px; + } + } + + .icon-close, + .icon-arrow { + height: rem(10px); + } + } + } + + &.-full-screen { + height: $full-screen-height; + + .nav { + height: $full-screen-height; + padding-right: rem(15px); + } + + .logo { + left: 0; + } + } +} diff --git a/app/javascript/components/map-v2/components/legend/components/timeline/styles.scss b/app/javascript/components/map-v2/components/legend/components/timeline/styles.scss index 5f13abc664..c8e6637d59 100644 --- a/app/javascript/components/map-v2/components/legend/components/timeline/styles.scss +++ b/app/javascript/components/map-v2/components/legend/components/timeline/styles.scss @@ -28,6 +28,11 @@ svg { fill: #4a4a4a; } + + .pause { + width: rem(10px); + height: rem(15px); + } } .play-btn:focus { diff --git a/app/javascript/components/ui/search/search-component.jsx b/app/javascript/components/ui/search/search-component.jsx index b6648f5a11..b7060ac791 100644 --- a/app/javascript/components/ui/search/search-component.jsx +++ b/app/javascript/components/ui/search/search-component.jsx @@ -28,6 +28,14 @@ class Search extends Component { this.debouncedChange(); }; + handleKeyUp = e => { + e.preventDefault(); + const { onSubmit } = this.props; + if (onSubmit && e.keyCode === 13) { + onSubmit(e); + } + }; + debouncedChange = debounce(() => { const { onChange } = this.props; if (onChange) { @@ -37,14 +45,7 @@ class Search extends Component { render() { const { search } = this.state; - const { - input, - placeholder, - handleKeyUp, - disabled, - className, - theme - } = this.props; + const { placeholder, onSubmit, disabled, className, theme } = this.props; return (
this.handleChange(e.target.value)} value={search} - onKeyUp={handleKeyUp} + onKeyUp={this.handleKeyUp} disabled={disabled} /> - + {search && ( )}
@@ -78,7 +81,7 @@ Search.propTypes = { input: PropTypes.string, placeholder: PropTypes.string, onChange: PropTypes.func, - handleKeyUp: PropTypes.func, + onSubmit: PropTypes.func, disabled: PropTypes.bool, className: PropTypes.string, theme: PropTypes.string diff --git a/app/javascript/components/ui/search/search-styles.scss b/app/javascript/components/ui/search/search-styles.scss index 5eaca8b3c1..783a72f8d6 100644 --- a/app/javascript/components/ui/search/search-styles.scss +++ b/app/javascript/components/ui/search/search-styles.scss @@ -8,7 +8,7 @@ height: 45px; border-radius: 100px; border: 0; - padding: 0 60px 0 20px; + padding: 0 65px 0 20px; font-size: 14px; &:focus { @@ -16,20 +16,22 @@ } } - .icon { + .icon-search { position: absolute; - right: 5px; - top: 5px; - width: 40px; - height: 40px; + right: 15px; + top: 13px; + width: 20px; + height: 20px; + cursor: pointer; } .clear-btn { position: absolute; - right: rem(35px); + right: rem(40px); top: rem(13px); + cursor: pointer; - .icon { + .icon-close { width: rem(10px); height: rem(10px); } diff --git a/app/javascript/pages/map-v2/menu/menu-component.jsx b/app/javascript/pages/map-v2/menu/menu-component.jsx index 452b030fe2..24da24d298 100644 --- a/app/javascript/pages/map-v2/menu/menu-component.jsx +++ b/app/javascript/pages/map-v2/menu/menu-component.jsx @@ -21,6 +21,7 @@ class Menu extends PureComponent { loading, countries, setMenuSettings, + toggleMenu, selectedCountries, countriesWithoutData } = this.props; @@ -29,11 +30,13 @@ class Menu extends PureComponent { return (
- Global Forest Watch + {sections && (
    - +
    + +
    @@ -33,6 +50,7 @@ class Page extends PureComponent { +
); } @@ -40,6 +58,9 @@ class Page extends PureComponent { Page.propTypes = { analysis: PropTypes.object, + handleShowMenu: PropTypes.func, + showHeader: PropTypes.bool, + loggedIn: PropTypes.bool, mapSettings: PropTypes.object }; diff --git a/app/javascript/pages/map-v2/page/page-styles.scss b/app/javascript/pages/map-v2/page/page-styles.scss index c2e0165c7a..64a76b804a 100644 --- a/app/javascript/pages/map-v2/page/page-styles.scss +++ b/app/javascript/pages/map-v2/page/page-styles.scss @@ -7,6 +7,14 @@ top: rem(15px); } + .map-header { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 1000; + } + .map { width: calc(100% - 76px); position: absolute; diff --git a/app/javascript/pages/map-v2/page/page.js b/app/javascript/pages/map-v2/page/page.js index cf8ecf1431..e3b6db771f 100644 --- a/app/javascript/pages/map-v2/page/page.js +++ b/app/javascript/pages/map-v2/page/page.js @@ -1,22 +1,38 @@ import { createElement, PureComponent } from 'react'; import { connect } from 'react-redux'; +import isEmpty from 'lodash/isEmpty'; import actions from 'components/map-v2/actions'; import { getMapSettings } from 'components/map-v2/selectors'; import PageComponent from './page-component'; -const mapStateToProps = ({ location, countryData, dataAnalysis }) => ({ +const mapStateToProps = ({ location, countryData, dataAnalysis, myGfw }) => ({ ...location, ...countryData, ...dataAnalysis, + loggedIn: !isEmpty(myGfw.data), mapSettings: getMapSettings(location) }); class PageContainer extends PureComponent { + constructor(props) { + super(props); + this.state = { + showHeader: false + }; + } + + handleShowMenu = () => { + const { showHeader } = this.state; + this.setState({ showHeader: !showHeader }); + }; + render() { return createElement(PageComponent, { - ...this.props + ...this.props, + ...this.state, + handleShowMenu: this.handleShowMenu }); } } diff --git a/app/javascript/pages/map-v2/store.js b/app/javascript/pages/map-v2/store.js index 173f424bc2..3a575e403e 100644 --- a/app/javascript/pages/map-v2/store.js +++ b/app/javascript/pages/map-v2/store.js @@ -21,6 +21,7 @@ import * as whitelistsProviderComponent from 'providers/whitelists-provider'; import * as datasetsProviderComponent from 'providers/datasets-provider'; import * as layerSpecProviderComponent from 'providers/layerspec-provider'; import * as latestProviderComponent from 'providers/latest-provider'; +import * as myGFWProviderComponent from 'providers/mygfw-provider'; // Component Reducers const componentsReducers = { @@ -39,7 +40,8 @@ const providersReducers = { whitelists: handleActions(whitelistsProviderComponent), datasets: handleActions(datasetsProviderComponent), layerSpec: handleActions(layerSpecProviderComponent), - latest: handleActions(latestProviderComponent) + latest: handleActions(latestProviderComponent), + myGfw: handleActions(myGFWProviderComponent) }; export const reducers = combineReducers({ diff --git a/app/javascript/pages/sgf/section-projects/section-projects.js b/app/javascript/pages/sgf/section-projects/section-projects.js index 5b71fcb7df..8695d0fb71 100644 --- a/app/javascript/pages/sgf/section-projects/section-projects.js +++ b/app/javascript/pages/sgf/section-projects/section-projects.js @@ -35,6 +35,7 @@ const mapStateToProps = ({ projects }) => { filters && filters.length ? '' : projects.categorySelected, search: projects.search, loading: + !projects || projects.loading || (projects.data.projects && !projects.data.projects.length) || !projects.data.images, diff --git a/app/javascript/providers/mygfw-provider/actions.js b/app/javascript/providers/mygfw-provider/actions.js new file mode 100644 index 0000000000..b32ddee648 --- /dev/null +++ b/app/javascript/providers/mygfw-provider/actions.js @@ -0,0 +1,26 @@ +import axios from 'axios'; +import { createAction } from 'redux-actions'; +import { createThunkAction } from 'utils/redux'; + +export const setMyGFWLoading = createAction('setMyGFWLoading'); +export const setMyGFW = createAction('setMyGFW'); + +export const getMyGFW = createThunkAction( + 'getMyGFW', + () => (dispatch, state) => { + if (!state().myGfw.loading) { + dispatch(setMyGFWLoading({ loading: true, error: false })); + axios + .get(`${process.env.GFW_API}/user`, { withCredentials: true }) + .then(response => { + if (response.status < 400) { + dispatch(setMyGFW(response.data.data.attributes)); + } + }) + .catch(err => { + dispatch(setMyGFWLoading({ loading: false, error: true })); + console.info('User not logged in', err); + }); + } + } +); diff --git a/app/javascript/providers/mygfw-provider/index.js b/app/javascript/providers/mygfw-provider/index.js new file mode 100644 index 0000000000..28ca86f4f4 --- /dev/null +++ b/app/javascript/providers/mygfw-provider/index.js @@ -0,0 +1,24 @@ +import { PureComponent } from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; + +import * as actions from './actions'; +import reducers, { initialState } from './reducers'; + +class MyGFWProvider extends PureComponent { + componentWillMount() { + const { getMyGFW } = this.props; + getMyGFW(); + } + + render() { + return null; + } +} + +MyGFWProvider.propTypes = { + getMyGFW: PropTypes.func.isRequired +}; + +export { actions, reducers, initialState }; +export default connect(null, actions)(MyGFWProvider); diff --git a/app/javascript/providers/mygfw-provider/reducers.js b/app/javascript/providers/mygfw-provider/reducers.js new file mode 100644 index 0000000000..8956a4d54c --- /dev/null +++ b/app/javascript/providers/mygfw-provider/reducers.js @@ -0,0 +1,22 @@ +export const initialState = { + loading: false, + error: false, + data: {} +}; + +const setMyGFWLoading = (state, { payload }) => ({ + ...state, + ...payload, + data: {} +}); + +const setMyGFW = (state, { payload }) => ({ + ...state, + data: payload, + loading: false +}); + +export default { + setMyGFW, + setMyGFWLoading +}; diff --git a/app/javascript/styles/text.scss b/app/javascript/styles/text.scss index aa8cb891eb..3bc5283bb4 100644 --- a/app/javascript/styles/text.scss +++ b/app/javascript/styles/text.scss @@ -1,5 +1,18 @@ @import '~styles/settings.scss'; +p { + font-size: rem(14px); + color: $slate; + + a { + color: $green-gfw; + + &:hover { + color: darken($green-gfw, 10%); + } + } +} + .text { font-family: $font-family-1;