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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 @@
+
+
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 ? (
+
+ ) : (
+
+
+
+ )}
+
+ {!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 (
+
+ {options &&
+ options.map(l => (
+ -
+ {handleSelect || l.onSelect ? (
+
+ ) : (
+
+ {l.label}
+
+ )}
+
+ ))}
+
+ );
+ }
+}
+
+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 (
+
+ );
+ }
+}
+
+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 ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+
+ 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 (
-
+
{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;