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 - - - {globalNavRevealed && isMediumDown && } - - - - - Earthdata {appTitle} - - - Beta - - - - {isMediumDown && ( - - setGlobalNavRevealed((v) => !v)} - active={globalNavRevealed} - > - - - - )} - - - {isMediumDown && ( - <> - - - - - )} - - - - Global - -
  • - - Data Catalog - -
  • -
  • - {checkEnvFlag(process.env.FEATURE_NEW_EXPLORATION) ? ( - - Exploration - - ) : ( - - Data Analysis - - )} -
  • -
  • - - {getString('stories').other} - -
  • - - {/* - Temporarily add hub link through env variables. - This does not scale for the different instances, but it's a - quick fix for the GHG app. - */} - {!!process.env.HUB_URL && !!process.env.HUB_NAME && ( -
  • - - {process.env.HUB_NAME} - -
  • - )} -
    -
    - - Meta - - {userPagesMainNavItem} -
  • - - About - -
  • - {!!process.env.GOOGLE_FORM && ( -
  • - -
  • - )} - - -
    -
    -
    -
    -
    -
    -
    - - ); -} - -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 + + + {globalNavRevealed && isMediumDown && } + {logo} + {isMediumDown && ( + + setGlobalNavRevealed((v) => !v)} + active={globalNavRevealed} + > + + + + )} + + + {isMediumDown && ( + <> + + + + + )} + + + + Global + + {mainNavItems.map((item) => { + return ; + })} + + + + Meta + + {subNavItems.map((item) => { + return ; + })} + + + + + + + + + ); +} + +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"