diff --git a/app/scripts/components/common/google-form.tsx b/app/scripts/components/common/google-form.tsx
index 6956cda6d..923c4ff80 100644
--- a/app/scripts/components/common/google-form.tsx
+++ b/app/scripts/components/common/google-form.tsx
@@ -42,7 +42,8 @@ const ButtonAsNavLink = styled(Button)`
`}
`;
-function GoogleForm() {
+const GoogleForm: React.FC<{ title: string, src: string }> = (props) => {
+ const { title, src } = props;
const { isRevealed, show, hide } = useFeedbackModal();
return (
@@ -53,7 +54,7 @@ function GoogleForm() {
onClick={show}
style={{ color: 'white' }}
>
- Contact Us
+ {title}
>
);
-}
+};
export default GoogleForm;
diff --git a/app/scripts/components/common/layout-root/index.tsx b/app/scripts/components/common/layout-root/index.tsx
index e77f9ca4d..6c44fa9f6 100644
--- a/app/scripts/components/common/layout-root/index.tsx
+++ b/app/scripts/components/common/layout-root/index.tsx
@@ -3,7 +3,6 @@ import { useDeepCompareEffect } from 'use-deep-compare';
import styled from 'styled-components';
import { Outlet } from 'react-router';
import { reveal } from '@devseed-ui/animation';
-
import MetaTags from '../meta-tags';
import PageFooter from '../page-footer';
import Banner from '../banner';
@@ -12,10 +11,14 @@ import { LayoutRootContext } from './context';
import { useGoogleTagManager } from '$utils/use-google-tag-manager';
import NavWrapper from '$components/common/nav-wrapper';
+import Logo from '$components/common/page-header/logo';
+import { mainNavItems, subNavItems} from '$components/common/page-header/default-config';
const appTitle = process.env.APP_TITLE;
const appDescription = process.env.APP_DESCRIPTION;
+
export const PAGE_BODY_ID = 'pagebody';
+
const Page = styled.div`
display: flex;
flex-direction: column;
@@ -51,7 +54,7 @@ function LayoutRoot(props: { children?: ReactNode }) {
thumbnail={thumbnail}
/>
{banner && }
-
+ } />
{children}
diff --git a/app/scripts/components/common/nav-wrapper.js b/app/scripts/components/common/nav-wrapper.js
index ef976938d..432d52e8f 100644
--- a/app/scripts/components/common/nav-wrapper.js
+++ b/app/scripts/components/common/nav-wrapper.js
@@ -27,16 +27,15 @@ const NavWrapper = styled.div`
`}
`;
-function PageNavWrapper() {
+function PageNavWrapper(props) {
const { isHeaderHidden, headerHeight } = useSlidingStickyHeaderProps();
-
return (
);
}
diff --git a/app/scripts/components/common/page-header.tsx b/app/scripts/components/common/page-header.tsx
deleted file mode 100644
index 02712b0a5..000000000
--- a/app/scripts/components/common/page-header.tsx
+++ /dev/null
@@ -1,605 +0,0 @@
-import React, { useCallback, useEffect, useRef, useState } from 'react';
-import styled, { css } from 'styled-components';
-import { Link, NavLink } from 'react-router-dom';
-import { userPages, getOverride, getString } from 'veda';
-import {
- glsp,
- listReset,
- media,
- rgba,
- themeVal,
- visuallyHidden
-} from '@devseed-ui/theme-provider';
-import { reveal } from '@devseed-ui/animation';
-import { Heading, Overline } from '@devseed-ui/typography';
-import { Button } from '@devseed-ui/button';
-import {
- CollecticonEllipsisVertical,
- CollecticonHamburgerMenu
-} from '@devseed-ui/collecticons';
-import { DropMenu, DropMenuItem } from '@devseed-ui/dropdown';
-
-import DropdownScrollable from './dropdown-scrollable';
-import NasaLogo from './nasa-logo';
-import GoogleForm from './google-form';
-import { Tip } from './tip';
-import UnscrollableBody from './unscrollable-body';
-
-import { checkEnvFlag } from '$utils/utils';
-import { variableGlsp } from '$styles/variable-utils';
-import {
- STORIES_PATH,
- DATASETS_PATH,
- ANALYSIS_PATH,
- ABOUT_PATH,
- EXPLORATION_PATH
-} from '$utils/routes';
-import { PAGE_BODY_ID } from '$components/common/layout-root';
-import GlobalMenuLinkCSS from '$styles/menu-link';
-import { useMediaQuery } from '$utils/use-media-query';
-import { HEADER_ID } from '$utils/use-sliding-sticky-header';
-import { ComponentOverride } from '$components/common/page-overrides';
-
-const rgbaFixed = rgba as any;
-
-const appTitle = process.env.APP_TITLE;
-const appVersion = process.env.APP_VERSION;
-
-const PageHeaderSelf = styled.header`
- display: flex;
- flex-flow: row nowrap;
- align-items: center;
- justify-content: space-between;
- gap: ${variableGlsp()};
- padding: ${variableGlsp(0.75, 1)};
- background: ${themeVal('color.primary')};
- animation: ${reveal} 0.32s ease 0s 1;
-
- &,
- &:visited {
- color: ${themeVal('color.surface')};
- }
-`;
-
-const Brand = styled.div`
- display: flex;
- flex-shrink: 0;
-
- a {
- display: grid;
- align-items: center;
- gap: ${glsp(0, 0.5)};
-
- &,
- &:visited {
- color: inherit;
- text-decoration: none;
- }
-
- #nasa-logo-neg-mono {
- opacity: 1;
- transition: all 0.32s ease 0s;
- }
-
- #nasa-logo-pos {
- opacity: 0;
- transform: translate(0, -100%);
- transition: all 0.32s ease 0s;
- }
-
- &:hover {
- opacity: 1;
-
- #nasa-logo-neg-mono {
- opacity: 0;
- }
-
- #nasa-logo-pos {
- opacity: 1;
- }
- }
-
- svg {
- grid-row: 1 / span 2;
- height: 2.5rem;
- width: auto;
-
- ${media.largeUp`
- transform: scale(1.125);
- `}
- }
-
- span:first-of-type {
- font-size: 0.875rem;
- line-height: 1rem;
- font-weight: ${themeVal('type.base.extrabold')};
- text-transform: uppercase;
- }
-
- span:last-of-type {
- grid-row: 2;
- font-size: 1.25rem;
- line-height: 1.5rem;
- font-weight: ${themeVal('type.base.regular')};
- letter-spacing: -0.025em;
- }
- }
-`;
-
-const PageTitleSecLink = styled(Link)`
- align-self: end;
- font-size: 0.75rem;
- font-weight: ${themeVal('type.base.bold')};
- line-height: 1rem;
- text-transform: uppercase;
- background: ${themeVal('color.surface')};
- padding: ${glsp(0, 0.25)};
- border-radius: ${themeVal('shape.rounded')};
- margin: ${glsp(0.125, 0.5)};
-
- &&,
- &&:visited {
- color: ${themeVal('color.primary')};
- }
-
- ${media.largeUp`
- margin: ${glsp(0, 0.5)};
- font-size: 0.875rem;
- line-height: 1.25rem;
- padding: 0 ${glsp(0.5)};
- `}
-`;
-
-const GlobalNav = styled.nav<{ revealed: boolean }>`
- position: fixed;
- inset: 0 0 0 auto;
- z-index: 900;
- display: flex;
- flex-flow: column nowrap;
- width: 20rem;
- margin-right: -20rem;
- transition: margin 0.24s ease 0s;
-
- ${({ revealed }) =>
- revealed &&
- css`
- & {
- margin-right: 0;
- }
- `}
-
- ${media.largeUp`
- position: static;
- flex: 1;
- margin: 0;
- }
-
- &:before {
- content: '';
- }
- `}
-
- /* Show page nav backdrop on small screens */
-
- &::after {
- content: '';
- position: absolute;
- inset: 0 0 0 auto;
- z-index: -1;
- background: transparent;
- width: 0;
- transition: background 0.64s ease 0s;
-
- ${({ revealed }) =>
- revealed &&
- css`
- ${media.mediumDown`
- background: ${themeVal('color.base-400a')};
- width: 200vw;
- `}
- `}
- }
-`;
-
-const GlobalNavInner = styled.div`
- display: flex;
- flex-direction: column;
- flex: 1;
- background-color: ${themeVal('color.primary')};
-
- ${media.mediumDown`
- box-shadow: ${themeVal('boxShadow.elevationD')};
- `}
-`;
-
-const GlobalNavHeader = styled.div`
- padding: ${variableGlsp(1)};
- box-shadow: inset 0 -1px 0 0 ${themeVal('color.surface-200a')};
- ${media.largeUp`
- display: none;
- `}
-`;
-
-const GlobalNavTitle = styled(Heading).attrs({
- as: 'span',
- size: 'small'
-})`
- /* styled-component */
-`;
-
-export const GlobalNavActions = styled.div`
- align-self: start;
- ${media.largeUp`
- display: none;
- `}
-`;
-
-export const GlobalNavToggle = styled(Button)`
- z-index: 2000;
-`;
-
-const GlobalNavBody = styled.div`
- display: flex;
- flex: 1;
-
- .shadow-top {
- background: linear-gradient(
- to top,
- ${themeVal('color.primary-600')}00 0%,
- ${themeVal('color.primary-600')} 100%
- );
- }
-
- .shadow-bottom {
- background: linear-gradient(
- to bottom,
- ${themeVal('color.primary-600')}00 0%,
- ${themeVal('color.primary-600')} 100%
- );
- }
-`;
-
-const GlobalNavBodyInner = styled.div`
- display: flex;
- flex-direction: column;
- flex: 1;
- gap: ${variableGlsp()};
- padding: ${variableGlsp(1, 0)};
-
- ${media.largeUp`
- flex-direction: row;
- justify-content: space-between;
- padding: 0;
- `}
-`;
-
-const NavBlock = styled.div`
- display: flex;
- flex-flow: column nowrap;
- gap: ${glsp(0.25)};
-
- ${media.largeUp`
- flex-direction: row;
- align-items: center;
- gap: ${glsp(1.5)};
- `}
-`;
-
-const SROnly = styled.a`
- height: 1px;
- left: -10000px;
- overflow: hidden;
- position: absolute;
- top: auto;
- width: 1px;
- color: ${themeVal('color.link')};
- &:focus {
- top: 0;
- left: 0;
- background-color: ${themeVal('color.surface')};
- padding: ${glsp(0.25)};
- height: auto;
- width: auto;
- }
-`;
-
-const SectionsNavBlock = styled(NavBlock)`
- /* styled-component */
-`;
-
-const GlobalNavBlockTitle = styled(Overline).attrs({
- as: 'span'
-})`
- ${visuallyHidden}
- display: block;
- padding: ${variableGlsp(1, 1, 0.25, 1)};
- color: currentColor;
- opacity: 0.64;
-
- ${media.largeUp`
- padding: 0;
- `}
-`;
-
-const GlobalMenu = styled.ul`
- ${listReset()}
- display: flex;
- flex-flow: column nowrap;
- gap: ${glsp(0.5)};
-
- ${media.largeUp`
- flex-direction: row;
- justify-content: flex-start;
- align-items: center;
- gap: ${glsp(1.5)};
- `}
-`;
-
-const GlobalMenuLink = styled(NavLink)`
- ${GlobalMenuLinkCSS}
-`;
-
-const DropMenuNavItem = styled(DropMenuItem)`
- &.active {
- background-color: ${rgbaFixed(themeVal('color.link'), 0.08)};
- }
-`;
-
-function PageHeader() {
- const { isMediumDown } = useMediaQuery();
-
- const [globalNavRevealed, setGlobalNavRevealed] = useState(false);
-
- const globalNavBodyRef = useRef(null);
- // Click listener for the whole global nav body so we can close it when clicking
- // the overlay on medium down media query.
- const onGlobalNavClick = useCallback((e) => {
- if (!globalNavBodyRef.current?.contains(e.target)) {
- setGlobalNavRevealed(false);
- }
- }, []);
-
- useEffect(() => {
- // Close global nav when media query changes.
- // NOTE: isMediumDown is returning document.body's width, not the whole window width
- // which conflicts with how mediaquery decides the width.
- // JSX element susing isMediumDown is also protected with css logic because of this.
- // ex. Look at GlobalNavActions
- if (!isMediumDown) setGlobalNavRevealed(false);
- }, [isMediumDown]);
-
- const closeNavOnClick = useCallback(() => {
- setGlobalNavRevealed(false);
- }, []);
-
- const userPagesMainNavItem = userPages.map((id) => {
- const page = getOverride(id as any);
- if (!(page?.data.mainNavItem && page.data.mainNavItem.navTitle)) return false;
-
- return (
-
-
- {page.data.mainNavItem.navTitle }
-
-
- );
- });
-
- function skipNav(e) {
- // a tag won't appear for keyboard focus without href
- // so we are preventing the default behaviour of a link here
- e.preventDefault();
- // Then find a next focusable element in pagebody,focus it.
- const pageBody = document.getElementById(PAGE_BODY_ID);
- if (pageBody) {
- pageBody.focus();
- }
- }
-
- return (
- <>
- Skip to main content
-
- >
- );
-}
-
-export default PageHeader;
-
-interface DotMenuItem {
- id: any;
- menu: string;
-}
-
-function UserPagesDotMenu(props: {
- isMediumDown: boolean;
- onItemClick: () => void;
-}) {
- const { isMediumDown, onItemClick } = props;
-
- const dotMenuItems = userPages.reduce((menuItems: DotMenuItem[], id: any) => {
- const page = getOverride(id as any);
- if (page?.data.menu)
- // eslint-disable-next-line fp/no-mutating-methods
- return menuItems.concat({
- id,
- menu: page.data.menu
- });
- return menuItems;
- }, []);
-
- if (!dotMenuItems.length) return <>{false}>;
-
- if (isMediumDown) {
- return (
- <>
- {dotMenuItems.map((menuItem) => {
- const page = getOverride(menuItem.id as any);
- if (!page?.data.menu) return false;
-
- return (
-
-
- {menuItem.menu}
-
-
- );
- })}
- >
- );
- }
-
- return (
- (
- // @ts-expect-error UI lib error. achromic-text does exit
-
-
-
- )}
- >
-
- {userPages.map((id) => {
- const page = getOverride(id as any);
- if (!page?.data.menu) return false;
-
- return (
-
-
- {page.data.menu}
-
-
- );
- })}
-
-
- );
-}
diff --git a/app/scripts/components/common/page-header/default-config.ts b/app/scripts/components/common/page-header/default-config.ts
new file mode 100644
index 000000000..fc48f5b8d
--- /dev/null
+++ b/app/scripts/components/common/page-header/default-config.ts
@@ -0,0 +1,53 @@
+import { getString, getNavItemsFromVedaConfig } from 'veda';
+import { InternalNavLink, ExternalNavLink, ModalNavLink, DropdownNavLink, NavItemType } from '$components/common/page-header/types.d';
+
+import { checkEnvFlag } from '$utils/utils';
+import {
+ STORIES_PATH,
+ DATASETS_PATH,
+ ANALYSIS_PATH,
+ EXPLORATION_PATH,
+ ABOUT_PATH
+} from '$utils/routes';
+
+let defaultMainNavItems:(ExternalNavLink | InternalNavLink | DropdownNavLink | ModalNavLink)[] = [{
+ title: 'Data Catalog',
+ to: DATASETS_PATH,
+ type: NavItemType.INTERNAL_LINK
+}, {
+ title: checkEnvFlag(process.env.FEATURE_NEW_EXPLORATION) ? 'Exploration' : 'Analysis',
+ to: checkEnvFlag(process.env.FEATURE_NEW_EXPLORATION) ? EXPLORATION_PATH : ANALYSIS_PATH,
+ type: NavItemType.INTERNAL_LINK
+}, {
+ title: getString('stories').other,
+ to: STORIES_PATH,
+ type: NavItemType.INTERNAL_LINK
+}];
+
+if (!!process.env.HUB_URL && !!process.env.HUB_NAME) defaultMainNavItems = [...defaultMainNavItems, {
+ title: process.env.HUB_NAME,
+ href: process.env.HUB_URL,
+ type: NavItemType.EXTERNAL_LINK
+} as ExternalNavLink];
+
+let defaultSubNavItems:(ExternalNavLink | InternalNavLink | DropdownNavLink | ModalNavLink)[] = [{
+ title: 'About',
+ to: ABOUT_PATH,
+ type: NavItemType.INTERNAL_LINK
+}];
+
+if (process.env.GOOGLE_FORM) {
+ defaultSubNavItems = [...defaultSubNavItems, {
+ title: 'Contact us',
+ src: process.env.GOOGLE_FORM,
+ type: NavItemType.MODAL
+ }];
+}
+
+const mainNavItems = getNavItemsFromVedaConfig()?.mainNavItems?? defaultMainNavItems;
+const subNavItems = getNavItemsFromVedaConfig()?.subNavItems?? defaultSubNavItems;
+
+export {
+ mainNavItems,
+ subNavItems
+};
\ No newline at end of file
diff --git a/app/scripts/components/common/page-header/index.tsx b/app/scripts/components/common/page-header/index.tsx
new file mode 100644
index 000000000..6e947694d
--- /dev/null
+++ b/app/scripts/components/common/page-header/index.tsx
@@ -0,0 +1,341 @@
+import React, { useCallback, useEffect, useRef, useState, ReactElement } from 'react';
+import styled, { css } from 'styled-components';
+import {
+ glsp,
+ listReset,
+ media,
+ themeVal,
+ visuallyHidden
+} from '@devseed-ui/theme-provider';
+import { reveal } from '@devseed-ui/animation';
+import { Heading, Overline } from '@devseed-ui/typography';
+import { Button } from '@devseed-ui/button';
+import {
+ CollecticonHamburgerMenu
+} from '@devseed-ui/collecticons';
+
+import UnscrollableBody from '../unscrollable-body';
+import NavMenuItem from './nav-menu-item';
+import { NavItem } from './types';
+
+import { variableGlsp } from '$styles/variable-utils';
+import { PAGE_BODY_ID } from '$components/common/layout-root';
+import { useMediaQuery } from '$utils/use-media-query';
+import { HEADER_ID } from '$utils/use-sliding-sticky-header';
+
+
+const PageHeaderSelf = styled.header`
+ display: flex;
+ flex-flow: row nowrap;
+ align-items: center;
+ justify-content: space-between;
+ gap: ${variableGlsp()};
+ padding: ${variableGlsp(0.75, 1)};
+ background: ${themeVal('color.primary')};
+ animation: ${reveal} 0.32s ease 0s 1;
+
+ &,
+ &:visited {
+ color: ${themeVal('color.surface')};
+ }
+`;
+
+
+const GlobalNav = styled.nav<{ revealed: boolean }>`
+ position: fixed;
+ inset: 0 0 0 auto;
+ z-index: 900;
+ display: flex;
+ flex-flow: column nowrap;
+ width: 20rem;
+ margin-right: -20rem;
+ transition: margin 0.24s ease 0s;
+
+ ${({ revealed }) =>
+ revealed &&
+ css`
+ & {
+ margin-right: 0;
+ }
+ `}
+
+ ${media.largeUp`
+ position: static;
+ flex: 1;
+ margin: 0;
+ }
+
+ &:before {
+ content: '';
+ }
+ `}
+
+ /* Show page nav backdrop on small screens */
+
+ &::after {
+ content: '';
+ position: absolute;
+ inset: 0 0 0 auto;
+ z-index: -1;
+ background: transparent;
+ width: 0;
+ transition: background 0.64s ease 0s;
+
+ ${({ revealed }) =>
+ revealed &&
+ css`
+ ${media.mediumDown`
+ background: ${themeVal('color.base-400a')};
+ width: 200vw;
+ `}
+ `}
+ }
+`;
+
+const GlobalNavInner = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ background-color: ${themeVal('color.primary')};
+
+ ${media.mediumDown`
+ box-shadow: ${themeVal('boxShadow.elevationD')};
+ `}
+`;
+
+const GlobalNavHeader = styled.div`
+ padding: ${variableGlsp(1)};
+ box-shadow: inset 0 -1px 0 0 ${themeVal('color.surface-200a')};
+ ${media.largeUp`
+ display: none;
+ `}
+`;
+
+const GlobalNavTitle = styled(Heading).attrs({
+ as: 'span',
+ size: 'small'
+})`
+ /* styled-component */
+`;
+
+export const GlobalNavActions = styled.div`
+ align-self: start;
+ ${media.largeUp`
+ display: none;
+ `}
+`;
+
+export const GlobalNavToggle = styled(Button)`
+ z-index: 2000;
+`;
+
+const GlobalNavBody = styled.div`
+ display: flex;
+ flex: 1;
+
+ .shadow-top {
+ background: linear-gradient(
+ to top,
+ ${themeVal('color.primary-600')}00 0%,
+ ${themeVal('color.primary-600')} 100%
+ );
+ }
+
+ .shadow-bottom {
+ background: linear-gradient(
+ to bottom,
+ ${themeVal('color.primary-600')}00 0%,
+ ${themeVal('color.primary-600')} 100%
+ );
+ }
+`;
+
+const GlobalNavBodyInner = styled.div`
+ display: flex;
+ flex-direction: column;
+ flex: 1;
+ gap: ${variableGlsp()};
+ padding: ${variableGlsp(1, 0)};
+
+ ${media.largeUp`
+ flex-direction: row;
+ justify-content: space-between;
+ padding: 0;
+ `}
+`;
+
+const NavBlock = styled.div`
+ display: flex;
+ flex-flow: column nowrap;
+ gap: ${glsp(0.25)};
+
+ ${media.largeUp`
+ flex-direction: row;
+ align-items: center;
+ gap: ${glsp(1.5)};
+ `}
+`;
+
+const SROnly = styled.a`
+ height: 1px;
+ left: -10000px;
+ overflow: hidden;
+ position: absolute;
+ top: auto;
+ width: 1px;
+ color: ${themeVal('color.link')};
+ &:focus {
+ top: 0;
+ left: 0;
+ background-color: ${themeVal('color.surface')};
+ padding: ${glsp(0.25)};
+ height: auto;
+ width: auto;
+ }
+`;
+
+const SectionsNavBlock = styled(NavBlock)`
+ /* styled-component */
+`;
+
+const GlobalNavBlockTitle = styled(Overline).attrs({
+ as: 'span'
+})`
+ ${visuallyHidden}
+ display: block;
+ padding: ${variableGlsp(1, 1, 0.25, 1)};
+ color: currentColor;
+ opacity: 0.64;
+
+ ${media.largeUp`
+ padding: 0;
+ `}
+`;
+
+const GlobalMenu = styled.ul`
+ ${listReset()}
+ display: flex;
+ flex-flow: column nowrap;
+ gap: ${glsp(0.5)};
+
+ ${media.largeUp`
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ gap: ${glsp(1.5)};
+ `}
+`;
+
+interface PageHeaderProps {
+ mainNavItems: NavItem[];
+ subNavItems: NavItem[];
+ logo: ReactElement
+}
+
+
+function PageHeader(props: PageHeaderProps) {
+ const { mainNavItems, subNavItems, logo } = props;
+ const { isMediumDown } = useMediaQuery();
+
+ const [globalNavRevealed, setGlobalNavRevealed] = useState(false);
+
+ const globalNavBodyRef = useRef(null);
+ // Click listener for the whole global nav body so we can close it when clicking
+ // the overlay on medium down media query.
+ const onGlobalNavClick = useCallback((e) => {
+ if (!globalNavBodyRef.current?.contains(e.target)) {
+ setGlobalNavRevealed(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ // Close global nav when media query changes.
+ // NOTE: isMediumDown is returning document.body's width, not the whole window width
+ // which conflicts with how mediaquery decides the width.
+ // JSX element susing isMediumDown is also protected with css logic because of this.
+ // ex. Look at GlobalNavActions
+ if (!isMediumDown) setGlobalNavRevealed(false);
+ }, [isMediumDown]);
+
+ const closeNavOnClick = useCallback(() => {
+ setGlobalNavRevealed(false);
+ }, []);
+
+ function skipNav(e) {
+ // a tag won't appear for keyboard focus without href
+ // so we are preventing the default behaviour of a link here
+ e.preventDefault();
+ // Then find a next focusable element in pagebody,focus it.
+ const pageBody = document.getElementById(PAGE_BODY_ID);
+ if (pageBody) {
+ pageBody.focus();
+ }
+ }
+
+ return (
+ <>
+ Skip to main content
+
+ >
+ );
+}
+
+export default PageHeader;
diff --git a/app/scripts/components/common/page-header/logo.tsx b/app/scripts/components/common/page-header/logo.tsx
new file mode 100644
index 000000000..ab923a66c
--- /dev/null
+++ b/app/scripts/components/common/page-header/logo.tsx
@@ -0,0 +1,114 @@
+import React from 'react';
+import styled from 'styled-components';
+import { glsp, media, themeVal } from '@devseed-ui/theme-provider';
+import { Link } from 'react-router-dom';
+import NasaLogo from '../nasa-logo';
+import { Tip } from '../tip';
+import { ComponentOverride } from '$components/common/page-overrides';
+
+const appTitle = process.env.APP_TITLE;
+const appVersion = process.env.APP_VERSION;
+
+const Brand = styled.div`
+ display: flex;
+ flex-shrink: 0;
+
+ a {
+ display: grid;
+ align-items: center;
+ gap: ${glsp(0, 0.5)};
+
+ &,
+ &:visited {
+ color: inherit;
+ text-decoration: none;
+ }
+
+ #nasa-logo-neg-mono {
+ opacity: 1;
+ transition: all 0.32s ease 0s;
+ }
+
+ #nasa-logo-pos {
+ opacity: 0;
+ transform: translate(0, -100%);
+ transition: all 0.32s ease 0s;
+ }
+
+ &:hover {
+ opacity: 1;
+
+ #nasa-logo-neg-mono {
+ opacity: 0;
+ }
+
+ #nasa-logo-pos {
+ opacity: 1;
+ }
+ }
+
+ svg {
+ grid-row: 1 / span 2;
+ height: 2.5rem;
+ width: auto;
+
+ ${media.largeUp`
+ transform: scale(1.125);
+ `}
+ }
+
+ span:first-of-type {
+ font-size: 0.875rem;
+ line-height: 1rem;
+ font-weight: ${themeVal('type.base.extrabold')};
+ text-transform: uppercase;
+ }
+
+ span:last-of-type {
+ grid-row: 2;
+ font-size: 1.25rem;
+ line-height: 1.5rem;
+ font-weight: ${themeVal('type.base.regular')};
+ letter-spacing: -0.025em;
+ }
+ }
+`;
+
+const PageTitleSecLink = styled(Link)`
+ align-self: end;
+ font-size: 0.75rem;
+ font-weight: ${themeVal('type.base.bold')};
+ line-height: 1rem;
+ text-transform: uppercase;
+ background: ${themeVal('color.surface')};
+ padding: ${glsp(0, 0.25)};
+ border-radius: ${themeVal('shape.rounded')};
+ margin: ${glsp(0.125, 0.5)};
+
+ &&,
+ &&:visited {
+ color: ${themeVal('color.primary')};
+ }
+
+ ${media.largeUp`
+ margin: ${glsp(0, 0.5)};
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+ padding: 0 ${glsp(0.5)};
+ `}
+`;
+
+export default function Logo () {
+ return (
+
+
+
+
+ Earthdata {appTitle}
+
+
+ Beta
+
+
+ );
+}
\ No newline at end of file
diff --git a/app/scripts/components/common/page-header/nav-menu-item.tsx b/app/scripts/components/common/page-header/nav-menu-item.tsx
new file mode 100644
index 000000000..2e6a8c726
--- /dev/null
+++ b/app/scripts/components/common/page-header/nav-menu-item.tsx
@@ -0,0 +1,148 @@
+import React from 'react';
+import styled from 'styled-components';
+import { NavLink } from 'react-router-dom';
+import {
+ glsp,
+ media,
+ rgba,
+ themeVal
+} from '@devseed-ui/theme-provider';
+import { Button } from '@devseed-ui/button';
+import { CollecticonChevronDownSmall } from '@devseed-ui/collecticons';
+import { DropMenu, DropMenuItem } from '@devseed-ui/dropdown';
+
+import DropdownScrollable from '../dropdown-scrollable';
+import GoogleForm from '../google-form';
+import { AlignmentEnum, InternalNavLink, ExternalNavLink, NavLinkItem, DropdownNavLink, ModalNavLink, NavItem, NavItemType } from './types.d';
+import GlobalMenuLinkCSS from '$styles/menu-link';
+import { useMediaQuery } from '$utils/use-media-query';
+
+const rgbaFixed = rgba as any;
+
+export const GlobalNavActions = styled.div`
+ align-self: start;
+ ${media.largeUp`
+ display: none;
+ `}
+`;
+
+const GlobalMenuItem = styled.span`
+ ${GlobalMenuLinkCSS}
+ cursor: default;
+ &:hover {
+ opacity: 1;
+ }
+`;
+
+export const GlobalNavToggle = styled(Button)`
+ z-index: 2000;
+`;
+
+const GlobalMenuLink = styled(NavLink)`
+ ${GlobalMenuLinkCSS}
+`;
+const GlobalMenuButton = styled(Button)`
+ ${GlobalMenuLinkCSS}
+`;
+
+const DropMenuNavItem = styled(DropMenuItem)`
+ &.active {
+ background-color: ${rgbaFixed(themeVal('color.link'), 0.08)};
+ }
+ ${media.largeDown`
+ padding-left ${glsp(2)};
+ &:hover {
+ color: inherit;
+ opacity: 0.64;
+ }
+ `}
+`;
+
+
+function LinkDropMenuNavItem({ child, onClick }: { child: NavLinkItem, onClick?:() => void}) {
+ const { title, type, ...rest } = child;
+ if (type === NavItemType.INTERNAL_LINK) {
+ return (
+
+
+ {title}
+
+
+ );
+ // In case a user inputs a wrong type
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ } else if (type === NavItemType.EXTERNAL_LINK) {
+ return (
+
+
+ {title}
+
+
+ );
+ } else throw Error('Invalid child Nav item type');
+}
+
+
+export default function NavMenuItem({ item, alignment, onClick }: {item: NavItem, alignment?: AlignmentEnum, onClick?: () => void }) {
+ const { isMediumDown } = useMediaQuery();
+ const { title, type, ...rest } = item;
+ if (type === NavItemType.INTERNAL_LINK) {
+ return (
+
+
+ {title}
+
+
+
+ );
+ } else if (item.type === NavItemType.EXTERNAL_LINK) {
+ return (
+
+
+ {title}
+
+
+
+ );
+ } else if (type === NavItemType.MODAL) {
+ return ( );
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ } else if (type === NavItemType.DROPDOWN) {
+ const { title } = item as DropdownNavLink;
+ // Mobile view
+ if (isMediumDown) {
+ return (
+ <>
+ {title}
+ {item.children.map((child) => {
+ return ;
+ })}
+ >
+ );
+ } else {
+ return (
+ (
+ // @ts-expect-error achromic text exists
+
+ {title}
+
+ )}
+ >
+
+ {(item as DropdownNavLink).children.map((child) => {
+ return ;
+ })}
+
+
+ );
+ }
+ } else throw Error('Invalid type for Nav Items');
+}
\ No newline at end of file
diff --git a/app/scripts/components/common/page-header/types.d.ts b/app/scripts/components/common/page-header/types.d.ts
new file mode 100644
index 000000000..546bf3db9
--- /dev/null
+++ b/app/scripts/components/common/page-header/types.d.ts
@@ -0,0 +1,33 @@
+export type AlignmentEnum = 'left' | 'right';
+
+export enum NavItemType {
+ INTERNAL_LINK= 'internalLink',
+ EXTERNAL_LINK= 'externalLink',
+ DROPDOWN= 'dropdown',
+ MODAL= 'modal'
+}
+
+export interface InternalNavLink {
+ title: string;
+ to: string;
+ type: NavItemType.INTERNAL_LINK;
+}
+export interface ExternalNavLink {
+ title: string;
+ href: string;
+ type: NavItemType.EXTERNAL_LINK;
+}
+export type NavLinkItem = (ExternalNavLink | InternalNavLink);
+export interface ModalNavLink {
+ title: string;
+ type: NavItemType.MODAL;
+ src: string;
+}
+
+export interface DropdownNavLink {
+ title: string;
+ type: NavItemType.DROPDOWN;
+ children: NavLinkItem[];
+}
+
+export type NavItem = (NavLinkItem | ModalNavLink | DropdownNavLink);
\ No newline at end of file
diff --git a/app/scripts/components/home/index.tsx b/app/scripts/components/home/index.tsx
index 3bf4f02af..975a446b1 100644
--- a/app/scripts/components/home/index.tsx
+++ b/app/scripts/components/home/index.tsx
@@ -5,7 +5,7 @@ import { Button } from '@devseed-ui/button';
import { glsp, listReset, media, themeVal } from '@devseed-ui/theme-provider';
import { Heading } from '@devseed-ui/typography';
import { CollecticonChevronRightSmall } from '@devseed-ui/collecticons';
-import { getOverride, getBanner } from 'veda';
+import { getOverride, getBannerFromVedaConfig } from 'veda';
import rootCoverImage from '../../../graphics/layout/root-welcome--cover.jpg';
@@ -131,7 +131,7 @@ const getCoverProps = () => {
function RootHome() {
const { show: showFeedbackModal } = useFeedbackModal();
- const banner = getBanner();
+ const banner = getBannerFromVedaConfig();
const renderBanner = !!banner && banner.text && banner.url && banner.expires;
return (
diff --git a/mock/veda.config.js b/mock/veda.config.js
index 4ba57a478..5ce345172 100644
--- a/mock/veda.config.js
+++ b/mock/veda.config.js
@@ -1,3 +1,71 @@
+const dotEnvConfig = require('dotenv').config();
+const { parsed: config } = dotEnvConfig;
+function checkEnvFlag(value) {
+ return (value ?? '').toLowerCase() === 'true';
+}
+
+let mainNavItems = [
+ {
+ title: 'Test',
+ type: 'dropdown',
+ children: [
+ {
+ title: 'test dropdown',
+ to: '/stories',
+ type: 'internalLink'
+ }
+ ]
+ },
+ {
+ title: 'Data Catalog',
+ to: '/data-catalog',
+ type: 'internalLink'
+ },
+ {
+ title: checkEnvFlag(config.FEATURE_NEW_EXPLORATION)
+ ? 'Exploration'
+ : 'Analysis',
+ to: checkEnvFlag(config.FEATURE_NEW_EXPLORATION)
+ ? '/exploration'
+ : '/analysis',
+ type: 'internalLink'
+ },
+ {
+ title: 'stories',
+ to: '/stories',
+ type: 'internalLink'
+ }
+];
+
+if (!!config.HUB_URL && !!config.HUB_NAME)
+ mainNavItems = [
+ ...mainNavItems,
+ {
+ title: process.env.HUB_NAME,
+ href: process.env.HUB_URL,
+ type: 'externalLink'
+ }
+ ];
+
+let subNavItems = [
+ {
+ title: 'About',
+ to: '/about',
+ type: 'internalLink'
+ }
+];
+
+if (config.GOOGLE_FORM) {
+ subNavItems = [
+ ...subNavItems,
+ {
+ title: 'Contact us',
+ src: config.GOOGLE_FORM,
+ type: 'modal'
+ }
+ ];
+}
+
module.exports = {
datasets: './datasets/*.data.mdx',
stories: './stories/*.stories.mdx',
@@ -22,5 +90,9 @@ module.exports = {
url: 'stories/emit-and-aviris-3',
expires: '2024-08-03T12:00:00-04:00',
type: 'info'
+ },
+ navItems: {
+ mainNavItems,
+ subNavItems
}
};
diff --git a/package.json b/package.json
index 080a0b05d..b34bcb9c2 100644
--- a/package.json
+++ b/package.json
@@ -165,6 +165,7 @@
"d3": "^7.6.1",
"d3-scale-chromatic": "^3.0.0",
"date-fns": "^2.28.0",
+ "dotenv": "^16.4.5",
"file-saver": "^2.0.5",
"focus-trap-react": "^10.2.3",
"framer-motion": "^10.12.21",
diff --git a/parcel-resolver-veda/index.d.ts b/parcel-resolver-veda/index.d.ts
index df7ae55ed..95bae9347 100644
--- a/parcel-resolver-veda/index.d.ts
+++ b/parcel-resolver-veda/index.d.ts
@@ -268,6 +268,29 @@ declare module 'veda' {
type?: BannerType;
}
+ interface InternalNavLink {
+ title: string;
+ to: string;
+ type: 'internalLink'
+ }
+ interface ExternalNavLink {
+ title: string;
+ href: string;
+ type: 'externalLink'
+ }
+ type NavLinkItem = (ExternalNavLink | InternalNavLink);
+ export interface ModalNavLink {
+ title: string;
+ type: 'modal';
+ src: string;
+ }
+
+ export interface DropdownNavLink {
+ title: string;
+ type: 'dropdown';
+ children: NavLinkItem[];
+ }
+
/**
* Named exports: datasets.
* Object with all the veda datasets keyed by the dataset id.
@@ -310,7 +333,8 @@ declare module 'veda' {
export const getBoolean: (variable: string) => boolean;
- export const getBanner: () => BannerData | undefined;
+ export const getBannerFromVedaConfig: () => BannerData | undefined;
+ export const getNavItemsFromVedaConfig: () => {mainNavItems: (NavLinkItem | ModalNavLink | DropdownNavLink)[]| undefined, subNavItems: (NavLinkItem | ModalNavLink | DropdownNavLink)[] | undefined } | undefined;
/**
* List of custom user defined pages.
diff --git a/parcel-resolver-veda/index.js b/parcel-resolver-veda/index.js
index afa4f8e25..5f4fc3374 100644
--- a/parcel-resolver-veda/index.js
+++ b/parcel-resolver-veda/index.js
@@ -195,7 +195,8 @@ module.exports = new Resolver({
)},
strings: ${JSON.stringify(withDefaultStrings(result.strings))},
booleans: ${JSON.stringify(withDefaultStrings(result.booleans))},
- banner: ${JSON.stringify(result.banner)}
+ banner: ${JSON.stringify(result.banner)},
+ navItems: ${JSON.stringify(result.navItems)}
};
export const theme = ${JSON.stringify(result.theme) || null};
@@ -213,7 +214,8 @@ module.exports = new Resolver({
export const getBoolean = (variable) => config.booleans[variable];
export const getConfig = () => config;
- export const getBanner = () => config.banner;
+ export const getBannerFromVedaConfig = () => config.banner;
+ export const getNavItemsFromVedaConfig = () => config.navItems;
export const datasets = ${generateMdxDataObject(datasetsImportData)};
export const stories = ${generateMdxDataObject(storiesImportData)};
diff --git a/yarn.lock b/yarn.lock
index 269165fbd..c1c891e94 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6993,6 +6993,11 @@ dotenv-expand@^5.1.0:
resolved "http://verdaccio.ds.io:4873/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0"
integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==
+dotenv@^16.4.5:
+ version "16.4.5"
+ resolved "http://verdaccio.ds.io:4873/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
+ integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
+
dotenv@^7.0.0:
version "7.0.0"
resolved "http://verdaccio.ds.io:4873/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c"