diff --git a/app/scripts/components/analysis/define/aoi-selector.tsx b/app/scripts/components/analysis/define/aoi-selector.tsx index a2e4e55a3..2915c87c4 100644 --- a/app/scripts/components/analysis/define/aoi-selector.tsx +++ b/app/scripts/components/analysis/define/aoi-selector.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { + RefObject, + useCallback, + useEffect, + useMemo, + useState +} from 'react'; import styled from 'styled-components'; import { FeatureCollection, Polygon } from 'geojson'; import bbox from '@turf/bbox'; @@ -38,7 +44,6 @@ import DropMenuItemButton from '$styles/drop-menu-item-button'; import { makeFeatureCollection } from '$components/common/aoi/utils'; import { variableGlsp } from '$styles/variable-utils'; - const MapContainer = styled.div` position: relative; border-radius: ${themeVal('shape.rounded')}; @@ -57,7 +62,7 @@ const AoiHeadActions = styled(FoldHeadActions)` `; interface AoiSelectorProps { - mapRef: React.RefObject; + mapRef: RefObject; qsAoi?: FeatureCollection; aoiDrawState: AoiState; onAoiEvent: AoiChangeListenerOverload; diff --git a/app/scripts/components/analysis/page-hero-analysis.tsx b/app/scripts/components/analysis/page-hero-analysis.tsx index c453f5470..fe3b4ea85 100644 --- a/app/scripts/components/analysis/page-hero-analysis.tsx +++ b/app/scripts/components/analysis/page-hero-analysis.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { ReactNode, useEffect, useState } from 'react'; import styled, { css } from 'styled-components'; import { glsp, @@ -162,11 +162,11 @@ const PageHeroActions = styled.div` interface PageHeroAnalysisProps { title: string; - description: React.ReactNode; + description: ReactNode; isHidden?: boolean; isResults?: boolean; aoi?: FeatureCollection; - renderActions?: ({ size }: { size: ButtonProps['size'] }) => React.ReactNode; + renderActions?: ({ size }: { size: ButtonProps['size'] }) => ReactNode; } function PageHeroAnalysis(props: PageHeroAnalysisProps) { diff --git a/app/scripts/components/analysis/page-hero-media.tsx b/app/scripts/components/analysis/page-hero-media.tsx index 463fb8acb..89335973f 100644 --- a/app/scripts/components/analysis/page-hero-media.tsx +++ b/app/scripts/components/analysis/page-hero-media.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useRef, useState } from 'react'; import styled, { useTheme } from 'styled-components'; -import mapboxgl from 'mapbox-gl'; +import { GeoJSONSource, MapboxOptions, Map as MapboxMap } from 'mapbox-gl'; import { FeatureCollection, Polygon } from 'geojson'; import bbox from '@turf/bbox'; import { shade } from 'polished'; @@ -22,7 +22,7 @@ const WORLD_POLYGON = [ [180, 90] ]; -const mapOptions: Partial = { +const mapOptions: Partial = { style: DEFAULT_MAP_STYLE_URL, logoPosition: 'bottom-right', interactive: false, @@ -38,7 +38,7 @@ interface PageHeroMediaProps { function PageHeroMedia(props: PageHeroMediaProps) { const { aoi, isHeaderStuck, ...rest } = props; const mapContainer = useRef(null); - const mapRef = useRef(null); + const mapRef = useRef(null); const [isMapLoaded, setMapLoaded] = useState(false); const theme = useTheme(); @@ -76,7 +76,7 @@ function PageHeroMedia(props: PageHeroMediaProps) { if (!shouldMount || !isMapLoaded || !mapRef.current) return; const aoiSource = mapRef.current.getSource('aoi') as - | mapboxgl.GeoJSONSource + | GeoJSONSource | undefined; // Convert to multipolygon to use the inverse shading trick. @@ -121,10 +121,10 @@ function PageHeroMedia(props: PageHeroMediaProps) { } else { const aoiSource = mapRef.current.getSource( 'aoi' - ) as mapboxgl.GeoJSONSource; + ) as GeoJSONSource; const aoiInverseSource = mapRef.current.getSource( 'aoi-inverse' - ) as mapboxgl.GeoJSONSource; + ) as GeoJSONSource; aoiSource.setData(aoi); aoiInverseSource.setData(aoiInverse); } diff --git a/app/scripts/components/analysis/results/analysis-head-actions.tsx b/app/scripts/components/analysis/results/analysis-head-actions.tsx index 943a7a1ba..0372793f1 100644 --- a/app/scripts/components/analysis/results/analysis-head-actions.tsx +++ b/app/scripts/components/analysis/results/analysis-head-actions.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import styled, { useTheme } from 'styled-components'; import { glsp, media, themeVal } from '@devseed-ui/theme-provider'; import { Dropdown, DropTitle } from '@devseed-ui/dropdown'; @@ -145,7 +145,7 @@ export default function AnalysisHeadActions(props: AnalysisHeadActionsProps) { {dataMetrics.map((metric) => { const active = !!activeMetrics.find((m) => m.id === metric.id); return ( - + {theme.color?.[metric.themeColor]} @@ -158,7 +158,7 @@ export default function AnalysisHeadActions(props: AnalysisHeadActionsProps) { {metric.label} - + ); })} diff --git a/app/scripts/components/analysis/results/chart-card-message.tsx b/app/scripts/components/analysis/results/chart-card-message.tsx index c6224b9ab..d7c348572 100644 --- a/app/scripts/components/analysis/results/chart-card-message.tsx +++ b/app/scripts/components/analysis/results/chart-card-message.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import styled, { css } from 'styled-components'; import { glsp, themeVal } from '@devseed-ui/theme-provider'; import { @@ -45,7 +45,7 @@ export function ChartCardNoMetric() { ); } -export function ChartCardAlert(props: { message: React.ReactNode }) { +export function ChartCardAlert(props: { message: ReactNode }) { return ( diff --git a/app/scripts/components/analysis/results/chart-card.tsx b/app/scripts/components/analysis/results/chart-card.tsx index 98c40418a..0cb54fb75 100644 --- a/app/scripts/components/analysis/results/chart-card.tsx +++ b/app/scripts/components/analysis/results/chart-card.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef, useMemo, MouseEvent } from 'react'; +import React, { useCallback, useRef, useMemo, MouseEvent, ReactNode } from 'react'; import { reverse } from 'd3'; import styled, { useTheme } from 'styled-components'; import { Link } from 'react-router-dom'; @@ -57,7 +57,7 @@ const InfoTipContent = styled.div` `; interface ChartCardProps { - title: React.ReactNode; + title: ReactNode; chartData: TimeseriesData; activeMetrics: DataMetric[]; availableDomain: [Date, Date]; diff --git a/app/scripts/components/common/aoi/use-aoi-controls.ts b/app/scripts/components/common/aoi/use-aoi-controls.ts index d0deeabdc..de4f7dba0 100644 --- a/app/scripts/components/common/aoi/use-aoi-controls.ts +++ b/app/scripts/components/common/aoi/use-aoi-controls.ts @@ -1,4 +1,4 @@ -import { useCallback, useState } from 'react'; +import { RefObject, useCallback, useState } from 'react'; import { useDeepCompareEffect } from 'use-deep-compare'; import { MapboxMapRef } from '../mapbox'; @@ -14,7 +14,7 @@ const DEFAULT_PARAMETERS = { }; export function useAoiControls( - mapRef: React.RefObject, + mapRef: RefObject, initialState: Partial = {} ) { const [aoi, setAoi] = useState({ diff --git a/app/scripts/components/common/blocks/index.tsx b/app/scripts/components/common/blocks/index.tsx index d4689fa0c..764f16de3 100644 --- a/app/scripts/components/common/blocks/index.tsx +++ b/app/scripts/components/common/blocks/index.tsx @@ -1,4 +1,10 @@ -import React from 'react'; +import React, { + Children, + Component, + ReactElement, + ReactNode, + createElement +} from 'react'; import styled from 'styled-components'; import { media } from '@devseed-ui/theme-provider'; @@ -229,7 +235,7 @@ const matchingBlocks = { interface BlockComponentProps { type?: string; - children: React.ReactElement[]; + children: ReactElement[]; } function BlockComponent(props: BlockComponentProps) { @@ -240,7 +246,7 @@ function BlockComponent(props: BlockComponentProps) { // to return matching block type // ex.
will result in 'wideFigure' const typeName = type ? type : 'default'; - const childrenAsArray = React.Children.toArray(children); + const childrenAsArray = Children.toArray(children); const childrenComponents: string[] = childrenAsArray.map( // @ts-expect-error type may not exist depending on the node, but the error @@ -321,20 +327,17 @@ function BlockComponent(props: BlockComponentProps) { throw new HintedError(contentTypeErrorMessage, hints); } - return React.createElement( - matchingBlocks[`${typeName}${childrenNames}`], - props - ); + return createElement(matchingBlocks[`${typeName}${childrenNames}`], props); } interface BlockErrorBoundaryProps { childToRender: any; passErrorToChild?: boolean; className?: string; - children?: React.ReactNode + children?: ReactNode; } -export class BlockErrorBoundary extends React.Component< +export class BlockErrorBoundary extends Component< BlockErrorBoundaryProps, { error: any } > { @@ -374,10 +377,10 @@ export class BlockErrorBoundary extends React.Component< } interface BlockWithErrorProps { - title?: React.ReactNode; - subtitle?: React.ReactNode; + title?: ReactNode; + subtitle?: ReactNode; className?: string; - children?: React.ReactNode + children?: ReactNode; } export default function BlockWithError(props: BlockWithErrorProps) { diff --git a/app/scripts/components/common/blocks/scrollytelling/chapter.tsx b/app/scripts/components/common/blocks/scrollytelling/chapter.tsx index 2a6307f65..b7ff57b48 100644 --- a/app/scripts/components/common/blocks/scrollytelling/chapter.tsx +++ b/app/scripts/components/common/blocks/scrollytelling/chapter.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import styled from 'styled-components'; import { themeVal, media, multiply } from '@devseed-ui/theme-provider'; import { ProjectionOptions } from 'veda'; @@ -20,7 +20,7 @@ export interface ChapterProps { projectionId?: ProjectionOptions['id']; projectionCenter?: ProjectionOptions['center']; projectionParallels?: ProjectionOptions['parallels']; - children: React.ReactNode; + children: ReactNode; } /* eslint-enable react/no-unused-prop-types */ diff --git a/app/scripts/components/common/blocks/scrollytelling/index.tsx b/app/scripts/components/common/blocks/scrollytelling/index.tsx index bff3ac36e..79051a32c 100644 --- a/app/scripts/components/common/blocks/scrollytelling/index.tsx +++ b/app/scripts/components/common/blocks/scrollytelling/index.tsx @@ -1,4 +1,7 @@ import React, { + Children, + FunctionComponent, + ReactElement, useCallback, useEffect, useMemo, @@ -11,8 +14,9 @@ import styled, { css } from 'styled-components'; import * as dateFns from 'date-fns'; import scrollama from 'scrollama'; import { CSSTransition, SwitchTransition } from 'react-transition-group'; +import { Map as MapboxMap } from 'mapbox-gl'; import { CollecticonCircleXmark } from '@devseed-ui/collecticons'; -import mapboxgl from 'mapbox-gl'; + import { BlockErrorBoundary } from '..'; import { chapterDisplayName, @@ -43,7 +47,7 @@ import { Basemap } from '$components/common/mapbox/layers/basemap'; type ResolvedLayer = { layer: Exclude; - Component: React.FunctionComponent | null; + Component: FunctionComponent | null; runtimeData: { datetime?: Date; id: string }; } | null; @@ -79,7 +83,7 @@ const TheChapters = styled(Hug)` */ function useChapterPropsFromChildren(children): ScrollyChapter[] { return useMemo(() => { - const chapters = React.Children.toArray(children) as React.ReactElement< + const chapters = Children.toArray(children) as ReactElement< ChapterProps, any >[]; @@ -264,7 +268,7 @@ function Scrollytelling(props) { useSlidingStickyHeaderProps(); const mapContainer = useRef(null); - const mapRef = useRef(null); + const mapRef = useRef(null); const [isMapLoaded, setMapLoaded] = useState(false); // Extract the props from the chapters. diff --git a/app/scripts/components/common/card.tsx b/app/scripts/components/common/card.tsx index b03d1ae0f..154927ac6 100644 --- a/app/scripts/components/common/card.tsx +++ b/app/scripts/components/common/card.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { MouseEventHandler, ReactNode } from 'react'; import styled, { css } from 'styled-components'; import { Link } from 'react-router-dom'; import { format } from 'date-fns'; @@ -32,8 +32,8 @@ export const CardList = styled.ol` ${listReset()} grid-column: 1 / -1; display: grid; - grid-template-columns: repeat(1, 1fr); gap: ${variableGlsp()}; + grid-template-columns: repeat(1, 1fr); ${media.mediumUp` grid-template-columns: repeat(2, 1fr); @@ -42,6 +42,10 @@ export const CardList = styled.ol` ${media.largeUp` grid-template-columns: repeat(3, 1fr); `} + + > li { + min-width: 0; + } `; function renderCardType({ cardType }: CardSelfProps) { @@ -69,6 +73,11 @@ function renderCardType({ cardType }: CardSelfProps) { padding-top: ${variableGlsp()}; color: ${themeVal('color.surface')}; justify-content: flex-end; + min-height: 16rem; + + ${media.mediumUp` + min-height: 28rem; + `} ${CardFigure} { position: absolute; @@ -169,6 +178,26 @@ export const CardOverline = styled(Overline)` } `; +export const CardMeta = styled.div` + display: flex; + gap: ${glsp(0.25)}; + + > a { + color: inherit; + pointer-events: all; + + &, + &:visited { + text-decoration: none; + color: inherit; + } + + &:hover { + opacity: 0.64; + } + } +`; + const CardLabel = styled.span` position: absolute; z-index: 1; @@ -202,6 +231,39 @@ export const CardBody = styled.div` } `; +export const CardFooter = styled.div` + display: flex; + flex-flow: row nowrap; + gap: ${variableGlsp(0.5)}; + padding: ${variableGlsp()}; + + &:not(:first-child) { + padding-top: 0; + margin-top: ${variableGlsp(-0.5)}; + } + + button { + pointer-events: all; + } +`; + +export const CardTopicsList = styled.dl` + display: flex; + gap: ${variableGlsp(0.25)}; + max-width: 100%; + width: 100%; + overflow: hidden; + mask-image: linear-gradient( + to right, + black calc(100% - 3rem), + transparent 100% + ); + + > dt { + ${visuallyHidden()} + } +`; + const CardFigure = styled(Figure)` order: -1; @@ -214,19 +276,20 @@ const CardFigure = styled(Figure)` `; interface CardComponentProps { - title: string; + title: ReactNode; linkLabel: string; linkTo: string; className?: string; cardType?: CardType; - description?: string; + description?: ReactNode; date?: Date; - overline?: React.ReactNode; + overline?: ReactNode; imgSrc?: string; imgAlt?: string; parentName?: string; parentTo?: string; - onCardClickCapture?: React.MouseEventHandler; + footerContent?: ReactNode; + onCardClickCapture?: MouseEventHandler; } function CardComponent(props: CardComponentProps) { @@ -243,6 +306,7 @@ function CardComponent(props: CardComponentProps) { imgAlt, parentName, parentTo, + footerContent, onCardClickCapture } = props; @@ -261,7 +325,7 @@ function CardComponent(props: CardComponentProps) { {title} - + {parentName && parentTo && ( {parentName} @@ -285,6 +349,7 @@ function CardComponent(props: CardComponentProps) {

{description}

)} + {footerContent && {footerContent}} {imgSrc && ( {imgAlt} diff --git a/app/scripts/components/common/chart/analysis/index.tsx b/app/scripts/components/common/chart/analysis/index.tsx index 133ba562b..47bc711ae 100644 --- a/app/scripts/components/common/chart/analysis/index.tsx +++ b/app/scripts/components/common/chart/analysis/index.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useMemo, useImperativeHandle } from 'react'; +import React, { useRef, useMemo, useImperativeHandle, MutableRefObject, forwardRef } from 'react'; import styled from 'styled-components'; import FileSaver from 'file-saver'; @@ -18,13 +18,13 @@ interface AnalysisChartProps extends CommonLineChartProps { } export interface AnalysisChartRef { - instanceRef: React.MutableRefObject; + instanceRef: MutableRefObject; saveAsImage: (name?: string) => Promise; } const syncId = 'analysis'; -export default React.forwardRef( +export default forwardRef( function AnalysisChart(props, ref) { const { timeSeriesData, dates, uniqueKeys, dateFormat, altTitle } = props; diff --git a/app/scripts/components/common/chart/index.tsx b/app/scripts/components/common/chart/index.tsx index bd28afad0..99ce48d43 100644 --- a/app/scripts/components/common/chart/index.tsx +++ b/app/scripts/components/common/chart/index.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo, useEffect } from 'react'; +import React, { useState, useMemo, useEffect, forwardRef } from 'react'; import styled from 'styled-components'; import { LineChart, @@ -95,7 +95,7 @@ function CustomCursor(props) { return ; } -export default React.forwardRef( +export default forwardRef( function RLineChart(props, ref) { const { chartData, diff --git a/app/scripts/components/common/dateslider/faders.tsx b/app/scripts/components/common/dateslider/faders.tsx index 6fb7cc288..70fda50ae 100644 --- a/app/scripts/components/common/dateslider/faders.tsx +++ b/app/scripts/components/common/dateslider/faders.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import { ScaleLinear } from 'd3'; import { DateSliderDataItem } from './constants'; @@ -41,7 +41,7 @@ export function FaderDefinition(props: FaderDefinitionProps) { const rightOpc = Math.max(fadePx * xTranslation + b2, 0); return ( - + - + ); } diff --git a/app/scripts/components/common/dropdown-scrollable.tsx b/app/scripts/components/common/dropdown-scrollable.tsx index 6be79df47..f401a2f9a 100644 --- a/app/scripts/components/common/dropdown-scrollable.tsx +++ b/app/scripts/components/common/dropdown-scrollable.tsx @@ -1,8 +1,9 @@ -import React from 'react'; +import React, { forwardRef, ReactNode } from 'react'; import styled from 'styled-components'; import { glsp } from '@devseed-ui/theme-provider'; import { Dropdown, + DropdownProps, DropdownRef, DropMenu, DropTitle @@ -31,11 +32,11 @@ const shadowScrollbarProps = { autoHeightMax: 320 }; -interface DropdownScrollableProps { - children?: React.ReactNode; +interface DropdownScrollableProps extends DropdownProps { + children?: ReactNode; } -export default React.forwardRef( +export default forwardRef( function DropdownScrollable(props, ref) { const { children, ...rest } = props; return ( diff --git a/app/scripts/components/common/empty-hub.tsx b/app/scripts/components/common/empty-hub.tsx index c837acee2..060168767 100644 --- a/app/scripts/components/common/empty-hub.tsx +++ b/app/scripts/components/common/empty-hub.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import styled, { useTheme } from 'styled-components'; import { CollecticonPage } from '@devseed-ui/collecticons'; import { themeVal } from '@devseed-ui/theme-provider'; @@ -17,7 +17,7 @@ const EmptyHubWrapper = styled.div` gap: ${variableGlsp(1)}; `; -export default function EmptyHub(props: { children: React.ReactNode }) { +export default function EmptyHub(props: { children: ReactNode }) { const theme = useTheme(); return ( diff --git a/app/scripts/components/common/fold.tsx b/app/scripts/components/common/fold.tsx index f691cea30..a00f7d393 100644 --- a/app/scripts/components/common/fold.tsx +++ b/app/scripts/components/common/fold.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ComponentProps, ReactNode } from 'react'; import styled from 'styled-components'; import { glsp, media, themeVal } from '@devseed-ui/theme-provider'; @@ -21,7 +21,8 @@ export const FoldGrid = styled(Hug)` padding-top: ${variableGlsp(2)}; padding-bottom: ${variableGlsp(2)}; - & + & { + & + &, + & + ${FoldBase} { padding-top: 0; } `; @@ -33,10 +34,14 @@ const FoldInner = styled(Constrainer)` export const FoldHeader = styled.div` grid-column: 1 / -1; display: flex; - flex-flow: row nowrap; + flex-flow: column nowrap; gap: ${variableGlsp()}; - justify-content: space-between; - align-items: flex-end; + + ${media.largeUp` + flex-flow: row nowrap; + justify-content: space-between; + align-items: flex-end; + `} > a { flex-shrink: 0; @@ -101,7 +106,7 @@ const Content = styled(VarProse)` `; function FoldComponent( - props: React.ComponentProps & { children: React.ReactNode } + props: ComponentProps & { children: ReactNode } ) { const { children, ...rest } = props; @@ -116,7 +121,7 @@ export const Fold = styled(FoldComponent)` /* Convert to styled-component: https://styled-components.com/docs/advanced#caveat */ `; -export function FoldProse(props: { children: React.ReactNode }) { +export function FoldProse(props: { children: ReactNode }) { const { children } = props; return ( diff --git a/app/scripts/components/common/item-truncate-count.tsx b/app/scripts/components/common/item-truncate-count.tsx index b16312376..853e8eebb 100644 --- a/app/scripts/components/common/item-truncate-count.tsx +++ b/app/scripts/components/common/item-truncate-count.tsx @@ -1,14 +1,14 @@ -import React from 'react'; +import React, { Fragment, ReactNode } from 'react'; interface ItemTruncateCountProps { - items: React.ReactNode[]; + items: ReactNode[]; max?: number; } export default function ItemTruncateCount(props: ItemTruncateCountProps) { const { items, max = 4 } = props; - if (!items.length) return ; + if (!items.length) return ; if (items.length === 1) return <>{items[0]}; const toRender = items.slice(0, max); diff --git a/app/scripts/components/common/layout-root.tsx b/app/scripts/components/common/layout-root.tsx index 827cb00bd..8d7b88312 100644 --- a/app/scripts/components/common/layout-root.tsx +++ b/app/scripts/components/common/layout-root.tsx @@ -1,4 +1,12 @@ -import React, { createContext, useCallback, useContext, useState } from 'react'; +import React, { + Dispatch, + ReactNode, + SetStateAction, + createContext, + useCallback, + useContext, + useState +} from 'react'; import { useDeepCompareEffect } from 'use-deep-compare'; import styled from 'styled-components'; import { Outlet } from 'react-router'; @@ -30,7 +38,7 @@ const PageBody = styled.div` overflow-anchor: auto; `; -function LayoutRoot(props: { children?: React.ReactNode }) { +function LayoutRoot(props: { children?: ReactNode }) { const { children } = props; useGoogleTagManager(); @@ -62,12 +70,12 @@ function LayoutRoot(props: { children?: React.ReactNode }) { export default LayoutRoot; interface LayoutRootContextProps extends Record { - setLayoutProps: React.Dispatch>>; + setLayoutProps: Dispatch>>; isHeaderHidden: boolean; headerHeight: number; wrapperHeight: number; feedbackModalRevealed: boolean; - setFeedbackModalRevealed: React.Dispatch>; + setFeedbackModalRevealed: Dispatch>; } // Context @@ -76,7 +84,7 @@ export const LayoutRootContext = createContext({} as LayoutRootContextProps); export function LayoutRootContextProvider({ children }: { - children: React.ReactNode; + children: ReactNode; }) { const [layoutProps, setLayoutProps] = useState>({}); const [feedbackModalRevealed, setFeedbackModalRevealed] = diff --git a/app/scripts/components/common/loading-skeleton.tsx b/app/scripts/components/common/loading-skeleton.tsx index 289296d43..fc3e4a46d 100644 --- a/app/scripts/components/common/loading-skeleton.tsx +++ b/app/scripts/components/common/loading-skeleton.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import styled, { keyframes, css } from 'styled-components'; import { glsp, themeVal, visuallyHidden } from '@devseed-ui/theme-provider'; import { CollecticonChartLine } from '@devseed-ui/collecticons'; @@ -221,7 +221,7 @@ export const MapLoading = (props) => { ); }; -export const ChartLoading = (props: { message: React.ReactNode }) => { +export const ChartLoading = (props: { message: ReactNode }) => { return ( diff --git a/app/scripts/components/common/mapbox/aoi/mb-aoi-draw.d.ts b/app/scripts/components/common/mapbox/aoi/mb-aoi-draw.d.ts index 6bc03a445..777fe58fc 100644 --- a/app/scripts/components/common/mapbox/aoi/mb-aoi-draw.d.ts +++ b/app/scripts/components/common/mapbox/aoi/mb-aoi-draw.d.ts @@ -1,5 +1,5 @@ import { MutableRefObject } from 'react'; -import mapboxgl from 'mapbox-gl'; +import { Map as MapboxMap } from 'mapbox-gl'; import { DefaultTheme, FlattenInterpolation, @@ -10,7 +10,7 @@ import { AoiChangeListener, AoiState } from '$components/common/aoi/types'; export const aoiCursorStyles: FlattenInterpolation>; type useMbDrawParams = { - mapRef: MutableRefObject; + mapRef: MutableRefObject; theme: DefaultTheme; onChange?: AoiChangeListener; } & Partial>; diff --git a/app/scripts/components/common/mapbox/aoi/mb-draw-popover.tsx b/app/scripts/components/common/mapbox/aoi/mb-draw-popover.tsx index d267b9806..77515d49b 100644 --- a/app/scripts/components/common/mapbox/aoi/mb-draw-popover.tsx +++ b/app/scripts/components/common/mapbox/aoi/mb-draw-popover.tsx @@ -1,4 +1,5 @@ import React, { MutableRefObject, useMemo } from 'react'; +import { Map as MapboxMap } from 'mapbox-gl'; import styled from 'styled-components'; import centroid from '@turf/centroid'; import { CollecticonTrashBin } from '@devseed-ui/collecticons'; @@ -13,7 +14,7 @@ const ActionPopoverInner = styled.div` `; interface MbDrawPopoverProps { - mapRef: MutableRefObject; + mapRef: MutableRefObject; onChange?: AoiChangeListener; selectedContext: AoiState['selectedContext']; } diff --git a/app/scripts/components/common/mapbox/index.tsx b/app/scripts/components/common/mapbox/index.tsx index ffa23ce59..58a3779ea 100644 --- a/app/scripts/components/common/mapbox/index.tsx +++ b/app/scripts/components/common/mapbox/index.tsx @@ -1,4 +1,6 @@ import React, { + ReactNode, + forwardRef, useCallback, useEffect, useImperativeHandle, @@ -7,7 +9,7 @@ import React, { useState } from 'react'; import styled from 'styled-components'; -import mapboxgl from 'mapbox-gl'; +import { Map as MapboxMap, MapboxOptions } from 'mapbox-gl'; import 'mapbox-gl/dist/mapbox-gl.css'; import CompareMbGL from 'mapbox-gl-compare'; import 'mapbox-gl-compare/dist/mapbox-gl-compare.css'; @@ -81,7 +83,7 @@ const MapsContainer = styled.div` } `; -const mapOptions: Partial = { +const mapOptions: Partial = { style: DEFAULT_MAP_STYLE_URL, logoPosition: 'bottom-left', trackResize: true, @@ -127,10 +129,10 @@ function MapboxMapComponent(props: MapboxMapProps, ref) { /* eslint-enable react/prop-types */ const mapContainer = useRef(null); - const mapRef = useRef(null); + const mapRef = useRef(null); const mapCompareContainer = useRef(null); - const mapCompareRef = useRef(null); + const mapCompareRef = useRef(null); const [isMapLoaded, setMapLoaded] = useState(false); const [isMapCompareLoaded, setMapCompareLoaded] = useState(false); @@ -502,7 +504,7 @@ export interface MapboxMapProps { } ) => void; withGeocoder?: boolean; - children?: React.ReactNode; + children?: ReactNode; aoi?: AoiState; onAoiChange?: AoiChangeListenerOverload; projection?: ProjectionOptions; @@ -511,11 +513,11 @@ export interface MapboxMapProps { export interface MapboxMapRef { resize: () => void; - instance: mapboxgl.Map | null; - compareInstance: mapboxgl.Map | null; + instance: MapboxMap | null; + compareInstance: MapboxMap | null; } -const MapboxMapComponentFwd = React.forwardRef( +const MapboxMapComponentFwd = forwardRef( MapboxMapComponent ); diff --git a/app/scripts/components/common/mapbox/layer-legend.tsx b/app/scripts/components/common/mapbox/layer-legend.tsx index 120c51d27..ef6792e17 100644 --- a/app/scripts/components/common/mapbox/layer-legend.tsx +++ b/app/scripts/components/common/mapbox/layer-legend.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Fragment } from 'react'; import styled from 'styled-components'; import { LayerLegendCategorical, LayerLegendGradient } from 'veda'; import { AccordionFold, AccordionManager } from '@devseed-ui/accordion'; @@ -231,7 +231,7 @@ function LayerCategoricalGraphic(props: LayerLegendCategorical) { return ( {stops.map((stop) => ( - +
@@ -250,7 +250,7 @@ function LayerCategoricalGraphic(props: LayerLegendCategorical) { {stop.label} - + ))} ); diff --git a/app/scripts/components/common/mapbox/layers/raster-timeseries.tsx b/app/scripts/components/common/mapbox/layers/raster-timeseries.tsx index 974e23bdb..5ac49202e 100644 --- a/app/scripts/components/common/mapbox/layers/raster-timeseries.tsx +++ b/app/scripts/components/common/mapbox/layers/raster-timeseries.tsx @@ -1,6 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import qs from 'qs'; -import mapboxgl, { +import { + Map as MapboxMap, AnyLayer, AnySourceImpl, GeoJSONSourceRaw, @@ -36,7 +37,7 @@ interface MapLayerRasterTimeseriesProps { id: string; stacCol: string; date?: Date; - mapInstance: mapboxgl.Map; + mapInstance: MapboxMap; sourceParams: object; zoomExtent?: [number, number]; onStatusChange?: (result: { status: ActionStatus; id: string }) => void; diff --git a/app/scripts/components/common/mapbox/layers/styles.tsx b/app/scripts/components/common/mapbox/layers/styles.tsx index 928b154a2..f5d4a8dee 100644 --- a/app/scripts/components/common/mapbox/layers/styles.tsx +++ b/app/scripts/components/common/mapbox/layers/styles.tsx @@ -1,5 +1,6 @@ import { AnyLayer, AnySourceImpl, Style } from 'mapbox-gl'; import React, { + ReactNode, createContext, useCallback, useContext, @@ -103,7 +104,7 @@ export function Styles({ children }: { onStyleUpdate?: (style: Style) => void; - children?: React.ReactNode; + children?: ReactNode; }) { const [stylesData, setStylesData] = useState>( {} diff --git a/app/scripts/components/common/mapbox/layers/utils.ts b/app/scripts/components/common/mapbox/layers/utils.ts index 16372fd73..0e171f7ac 100644 --- a/app/scripts/components/common/mapbox/layers/utils.ts +++ b/app/scripts/components/common/mapbox/layers/utils.ts @@ -1,6 +1,6 @@ -import React, { useEffect } from 'react'; +import { FunctionComponent, useEffect } from 'react'; import { Feature } from 'geojson'; -import mapboxgl from 'mapbox-gl'; +import { Map as MapboxMap } from 'mapbox-gl'; import { defaultsDeep } from 'lodash'; import axios, { Method } from 'axios'; import { @@ -30,7 +30,7 @@ import { HintedError } from '$utils/hinted-error'; export const getLayerComponent = ( isTimeseries: boolean, layerType: 'raster' | 'vector' -): React.FunctionComponent | null => { +): FunctionComponent | null => { if (isTimeseries) { if (layerType === 'raster') return MapLayerRasterTimeseries; if (layerType === 'vector') return MapLayerVectorTimeseries; @@ -350,7 +350,7 @@ export function getMergedBBox(features: StacFeature[]) { export function checkFitBoundsFromLayer( layerBounds?: [number, number, number, number], - mapInstance?: mapboxgl.Map + mapInstance?: MapboxMap ) { if (!layerBounds || !mapInstance) return false; @@ -374,7 +374,7 @@ export function checkFitBoundsFromLayer( interface LayerInteractionHookOptions { layerId: string; - mapInstance: mapboxgl.Map; + mapInstance: MapboxMap; onClick: (features: Feature[]) => void; } export function useLayerInteraction({ diff --git a/app/scripts/components/common/mapbox/layers/vector-timeseries.tsx b/app/scripts/components/common/mapbox/layers/vector-timeseries.tsx index ecac3b24b..9c41ed463 100644 --- a/app/scripts/components/common/mapbox/layers/vector-timeseries.tsx +++ b/app/scripts/components/common/mapbox/layers/vector-timeseries.tsx @@ -1,7 +1,8 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTheme } from 'styled-components'; import qs from 'qs'; -import mapboxgl, { +import { + Map as MapboxMap, AnyLayer, AnySourceImpl, LngLatLike, @@ -21,7 +22,7 @@ interface MapLayerVectorTimeseriesProps { id: string; stacCol: string; date?: Date; - mapInstance: mapboxgl.Map; + mapInstance: MapboxMap; sourceParams: object; zoomExtent?: [number, number]; onStatusChange?: (result: { status: ActionStatus; id: string }) => void; diff --git a/app/scripts/components/common/mapbox/map-message.tsx b/app/scripts/components/common/mapbox/map-message.tsx index ea2951f19..404f11668 100644 --- a/app/scripts/components/common/mapbox/map-message.tsx +++ b/app/scripts/components/common/mapbox/map-message.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import styled, { css } from 'styled-components'; import { Transition, TransitionGroup } from 'react-transition-group'; @@ -61,7 +61,7 @@ const Message = styled.div` interface MapMessageProps extends Pick { id: string; active: boolean; - children: React.ReactNode; + children: ReactNode; } export default function MapMessage(props: MapMessageProps) { diff --git a/app/scripts/components/common/mapbox/map.tsx b/app/scripts/components/common/mapbox/map.tsx index 1978fdb9b..f02946a36 100644 --- a/app/scripts/components/common/mapbox/map.tsx +++ b/app/scripts/components/common/mapbox/map.tsx @@ -5,7 +5,13 @@ import React, { ReactElement } from 'react'; import styled, { useTheme } from 'styled-components'; -import mapboxgl from 'mapbox-gl'; +import mapboxgl, { + Map as MapboxMap, + AttributionControl, + EventData, + MapboxOptions, + NavigationControl +} from 'mapbox-gl'; import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder'; import 'mapbox-gl/dist/mapbox-gl.css'; import { ProjectionOptions } from 'veda'; @@ -35,12 +41,12 @@ const SingleMapContainer = styled.div` interface SimpleMapProps { [key: string]: unknown; - mapRef: MutableRefObject; + mapRef: MutableRefObject; containerRef: RefObject; - onLoad?(e: mapboxgl.EventData): void; - onMoveEnd?(e: mapboxgl.EventData): void; + onLoad?(e: EventData): void; + onMoveEnd?(e: EventData): void; onUnmount?: () => void; - mapOptions: Partial>; + mapOptions: Partial>; withGeocoder?: boolean; aoi?: AoiState; onAoiChange?: AoiChangeListenerOverload; @@ -112,7 +118,7 @@ export function SimpleMap(props: SimpleMapProps): ReactElement { useEffect(() => { if (!containerRef.current) return; - const mbMap = new mapboxgl.Map({ + const mbMap = new MapboxMap({ container: containerRef.current, attributionControl: false, projection: projection && convertProjectionToMapbox(projection), @@ -145,7 +151,7 @@ export function SimpleMap(props: SimpleMapProps): ReactElement { // Add zoom controls without compass. if (mapOptions?.interactive !== false) { mbMap.addControl( - new mapboxgl.NavigationControl({ showCompass: false }), + new NavigationControl({ showCompass: false }), 'top-left' ); } @@ -175,7 +181,7 @@ export function SimpleMap(props: SimpleMapProps): ReactElement { useEffect(() => { if (!mapRef.current || !attributionPosition) return; - const ctrl = new mapboxgl.AttributionControl(); + const ctrl = new AttributionControl(); mapRef.current.addControl(ctrl, attributionPosition); return () => { mapRef.current?.removeControl(ctrl); diff --git a/app/scripts/components/common/mapbox/mb-popover/maker.ts b/app/scripts/components/common/mapbox/mb-popover/maker.ts index 4f821c394..39736b5d0 100644 --- a/app/scripts/components/common/mapbox/mb-popover/maker.ts +++ b/app/scripts/components/common/mapbox/mb-popover/maker.ts @@ -1,8 +1,8 @@ -import mapboxgl from 'mapbox-gl'; +import { Marker, MarkerOptions, Map as MapboxMap } from 'mapbox-gl'; type MapboxMarkerClickableListener = (coords: [number, number]) => void; -interface MapboxMarkerClickable extends mapboxgl.Marker { +interface MapboxMarkerClickable extends Marker { onClick: (listener: MapboxMarkerClickableListener) => MapboxMarkerClickable; } @@ -25,11 +25,8 @@ interface MapboxMarkerClickable extends mapboxgl.Marker { * @param {object} map Mapbox ma instance. * @param {object} opt Mapbox marker options as defined in the documentation. */ -export const createMbMarker = ( - map: mapboxgl.Map, - opt?: mapboxgl.MarkerOptions -) => { - const mk = new mapboxgl.Marker(opt); +export const createMbMarker = (map: MapboxMap, opt?: MarkerOptions) => { + const mk = new Marker(opt); let onClickListener: MapboxMarkerClickableListener | undefined; const onMapClick = (e) => { diff --git a/app/scripts/components/common/mapbox/mb-popover/popover-inner.tsx b/app/scripts/components/common/mapbox/mb-popover/popover-inner.tsx index fde558127..009f39b10 100644 --- a/app/scripts/components/common/mapbox/mb-popover/popover-inner.tsx +++ b/app/scripts/components/common/mapbox/mb-popover/popover-inner.tsx @@ -1,4 +1,10 @@ -import React, { useCallback, useState, useEffect, useRef } from 'react'; +import React, { + useCallback, + useState, + useEffect, + useRef, + ReactNode +} from 'react'; import ReactDOM from 'react-dom'; import { Toolbar, ToolbarIconButton } from '@devseed-ui/toolbar'; import { CollecticonXmarkSmall } from '@devseed-ui/collecticons'; @@ -24,7 +30,7 @@ import { const Try = ( props: { fn?: PopoverRenderFunction; - children: React.ReactNode; + children: ReactNode; } & PopoverRenderFunctionBag ) => { const { fn, children, ...rest } = props; diff --git a/app/scripts/components/common/mapbox/mb-popover/types.d.ts b/app/scripts/components/common/mapbox/mb-popover/types.d.ts index 634574a38..d1525dce9 100644 --- a/app/scripts/components/common/mapbox/mb-popover/types.d.ts +++ b/app/scripts/components/common/mapbox/mb-popover/types.d.ts @@ -1,4 +1,5 @@ -import mapboxgl from 'mapbox-gl'; +import { Map as MapboxMap } from 'mapbox-gl'; +import { ReactNode } from 'react'; export interface PopoverRenderFunctionBag { close: () => void; @@ -6,13 +7,13 @@ export interface PopoverRenderFunctionBag { export type PopoverRenderFunction = ( bag: PopoverRenderFunctionBag -) => React.ReactNode; +) => ReactNode; export interface MBPopoverProps { /** * Mapbox map which the popover will use. */ - mbMap: mapboxgl.Map; + mbMap: MapboxMap; /** * Coordinates the popover points to. */ @@ -40,7 +41,7 @@ export interface MBPopoverProps { * Title for the popover. * Required unless the header is being overridden. */ - title?: React.ReactNode; + title?: ReactNode; /** * Heading level for the popover. * @default h2 @@ -49,21 +50,21 @@ export interface MBPopoverProps { /** * Subtitle for the popover. It is displayed below the title. */ - subtitle?: React.ReactNode; + subtitle?: ReactNode; /** * Suptitle for the popover. It is displayed above the title. If both subtitle * and suptitle are present, the suptitle gets ignored. */ - suptitle?: React.ReactNode; + suptitle?: ReactNode; /** * Popover body content, rendered inside `PopoverBody`. Required unless the * body is being overridden. */ - content?: React.ReactNode; + content?: ReactNode; /** * Popover footer content, rendered inside `PopoverFooter`. */ - footerContent?: React.ReactNode; + footerContent?: ReactNode; /** * Vertical offset for the popover. The array must have 2 values. The first * for the top offset the second for the bottom offset. diff --git a/app/scripts/components/common/mapbox/use-mapbox-control.tsx b/app/scripts/components/common/mapbox/use-mapbox-control.tsx index f49180d36..8b67538d6 100644 --- a/app/scripts/components/common/mapbox/use-mapbox-control.tsx +++ b/app/scripts/components/common/mapbox/use-mapbox-control.tsx @@ -1,4 +1,4 @@ -import mapboxgl from 'mapbox-gl'; +import { IControl } from 'mapbox-gl'; import React, { useEffect, useMemo, useRef } from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { ThemeProvider, useTheme } from 'styled-components'; @@ -57,5 +57,5 @@ export function useMapboxControl(renderFn, deps:Array = []) { } }), [] - ) as mapboxgl.IControl; + ) as IControl; } diff --git a/app/scripts/components/common/notebook-connect.tsx b/app/scripts/components/common/notebook-connect.tsx index 5affeff25..49a426765 100644 --- a/app/scripts/components/common/notebook-connect.tsx +++ b/app/scripts/components/common/notebook-connect.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useState } from 'react'; +import React, { ReactNode, useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { media, multiply, themeVal } from '@devseed-ui/theme-provider'; import { Button, ButtonProps } from '@devseed-ui/button'; @@ -8,7 +8,7 @@ import { CollecticonCog } from '@devseed-ui/collecticons'; import { Modal } from '@devseed-ui/modal'; -import { DatasetData, datasets, VedaDatum } from 'veda'; +import { DatasetData, datasets } from 'veda'; import { HintedError } from '$utils/hinted-error'; import { variableGlsp } from '$styles/variable-utils'; @@ -17,7 +17,7 @@ interface NotebookConnectButtonProps { compact?: boolean; variation?: ButtonProps['variation']; size?: ButtonProps['size']; - dataset?: VedaDatum; + dataset?: DatasetData; className?: string; } @@ -86,19 +86,14 @@ const IconByType: Record = { unknown: }; -function NotebookConnectButtonSelf(props: NotebookConnectButtonProps) { - const { - className, - compact = true, - variation = 'primary-fill', - size = 'medium', - dataset - } = props; +export function NotebookConnectModal(props: { + dataset?: DatasetData; + revealed: boolean; + onClose: () => void; +}) { + const { dataset, revealed, onClose } = props; - const [revealed, setRevealed] = useState(false); - const close = useCallback(() => setRevealed(false), []); - - const datasetUsages = dataset?.data.usage; + const datasetUsages = dataset?.usage; const datasetUsagesWithIcon = useMemo(() => { return datasetUsages?.map((d) => { @@ -116,11 +111,70 @@ function NotebookConnectButtonSelf(props: NotebookConnectButtonProps) { return null; } - const layerIdsSet = dataset.data.layers.reduce( + const layerIdsSet = dataset.layers.reduce( (acc, layer) => acc.add(layer.stacCol), new Set() ); + return ( + + + {datasetUsagesWithIcon.map((datasetUsage) => ( +
  • + + {IconByType[datasetUsage.type]} + +

    {datasetUsage.title}

    +

    {datasetUsage.label}

    +
    +
    +
  • + ))} +
    +

    + For reference, the following STAC collection ID's are + associated with this dataset: +

    +
      + {Array.from(layerIdsSet).map((id) => ( +
    • + {id} +
    • + ))} +
    + + } + /> + ); +} + +function NotebookConnectButtonSelf(props: NotebookConnectButtonProps) { + const { + className, + compact = true, + variation = 'primary-fill', + size = 'medium', + dataset + } = props; + + const [revealed, setRevealed] = useState(false); + const close = useCallback(() => setRevealed(false), []); + + const datasetUsages = dataset?.usage; + + if (!datasetUsages) { + return null; + } + return ( <> - - - {datasetUsagesWithIcon.map((datasetUsage) => ( -
  • - - {IconByType[datasetUsage.type]} - -

    {datasetUsage.title}

    -

    {datasetUsage.label}

    -
    -
    -
  • - ))} -
    -

    - For reference, the following STAC collection ID's are - associated with this dataset: -

    -
      - {Array.from(layerIdsSet).map((id) => ( -
    • - {id} -
    • - ))} -
    - - } + onClose={close} /> ); @@ -180,7 +202,7 @@ export const NotebookConnectButton = styled(NotebookConnectButtonSelf)` `; interface NotebookConnectCalloutProps { - children: React.ReactNode; + children: ReactNode; datasetId: string; className?: string; } @@ -214,7 +236,7 @@ function NotebookConnectCalloutSelf(props: NotebookConnectCalloutProps) { ); diff --git a/app/scripts/components/common/page-overrides.tsx b/app/scripts/components/common/page-overrides.tsx index 45e8e1ab7..270e59481 100644 --- a/app/scripts/components/common/page-overrides.tsx +++ b/app/scripts/components/common/page-overrides.tsx @@ -1,4 +1,4 @@ -import React, { lazy } from 'react'; +import React, { lazy, ReactNode } from 'react'; import { MDXProvider } from '@mdx-js/react'; import { getOverride, PageOverrides } from 'veda'; @@ -10,7 +10,7 @@ const MdxContent = lazy(() => import('./mdx-content')); interface ComponentOverrideProps { [key: string]: any; with: PageOverrides; - children: React.ReactNode; + children: ReactNode; } export function ComponentOverride(props: ComponentOverrideProps) { diff --git a/app/scripts/components/common/search-field.tsx b/app/scripts/components/common/search-field.tsx new file mode 100644 index 000000000..06a50d8bb --- /dev/null +++ b/app/scripts/components/common/search-field.tsx @@ -0,0 +1,148 @@ +import React, { InputHTMLAttributes, useRef, useState } from 'react'; +import styled, { css } from 'styled-components'; +import { + FormHelperMessage, + FormInput, + formSkinStylesProps +} from '@devseed-ui/form'; +import { Button } from '@devseed-ui/button'; +import { + CollecticonDiscXmark, + CollecticonMagnifierLeft +} from '@devseed-ui/collecticons'; +import { themeVal } from '@devseed-ui/theme-provider'; + +const SearchFieldWrapper = styled.div` + display: flex; + flex-flow: column; + align-items: flex-end; +`; + +interface SearchFieldMessageProps { + isOpen: boolean; + isFocused: boolean; +} + +const SearchFieldMessage = styled(FormHelperMessage)` + line-height: 1.5rem; + transition: max-width 320ms ease-in-out, opacity 160ms ease-in-out 160ms; + white-space: nowrap; + max-width: 0; + opacity: 0; + + ${({ isOpen }) => + isOpen && + css` + max-width: 15rem; + `} + + ${({ isFocused }) => + isFocused && + css` + opacity: 1; + `} +`; + +const SearchFieldContainer = styled.div` + position: relative; + display: flex; + + ::before { + position: absolute; + inset: 0; + pointer-events: none; + display: block; + content: ''; + border-radius: ${themeVal('shape.rounded')}; + box-shadow: inset 0 0 0 1px ${themeVal('color.base')}; + } +`; + +const SearchFieldClearable = styled.div<{ isOpen: boolean }>` + display: flex; + overflow: hidden; + transition: max-width 320ms ease-in-out; + max-width: 0; + + ${({ isOpen }) => + isOpen && + css` + max-width: 15rem; + `} +`; + +const FormInputSearch = styled(FormInput)` + border: 0; + padding-left: 0; + padding-right: 0; + width: 15rem; +`; + +interface SearchFieldProps + extends Omit< + InputHTMLAttributes, + 'size' | 'onChange' + > { + size: formSkinStylesProps['size']; + value: string; + onChange: (value: string) => void; + keepOpen?: boolean; +} + +function SearchField(props: SearchFieldProps) { + const { value, onChange, keepOpen, size, ...rest } = props; + + const fieldRef = useRef(null); + const [isFocused, setFocused] = useState(false); + + const isOpen = isFocused || !!value.length || !!keepOpen; + + return ( + + + + + onChange(e.target.value)} + onFocus={() => { + setFocused(true); + }} + onBlur={() => { + setFocused(false); + }} + /> + + {!!value.length && ( + + )} + + + + Minimum 3 characters + + + ); +} + +export default SearchField; diff --git a/app/scripts/components/common/stressed-field.ts b/app/scripts/components/common/stressed-field.ts index c2b31806c..19db38105 100644 --- a/app/scripts/components/common/stressed-field.ts +++ b/app/scripts/components/common/stressed-field.ts @@ -1,22 +1,30 @@ -import { useCallback, useEffect, useRef } from 'react'; +import { + Dispatch, + FormEvent, + KeyboardEvent, + Ref, + useCallback, + useEffect, + useRef +} from 'react'; import T from 'prop-types'; import { useSafeState } from '$utils/use-safe-state'; interface StressedFieldProps { render: (renderProps: { - ref: React.Ref + ref: Ref; errored: boolean; value: string; handlers: { - onKeyPress: (e: React.KeyboardEvent) => void; + onKeyPress: (e: KeyboardEvent) => void; onBlur: () => void; - onChange: (e: React.FormEvent) => void; + onChange: (e: FormEvent) => void; }; }) => JSX.Element; value: string; validate: (value: string) => boolean; - onChange: (draftValue: string, setDraftValue: React.Dispatch) => void; + onChange: (draftValue: string, setDraftValue: Dispatch) => void; } /** @@ -67,13 +75,12 @@ export default function StressedField(props: StressedFieldProps) { // setDraftValue is a hook and wont change. const onChangeHandler = useCallback( - (e: React.FormEvent) => - setDraftValue(e.currentTarget.value), + (e: FormEvent) => setDraftValue(e.currentTarget.value), /* eslint-disable-next-line react-hooks/exhaustive-deps */ [] ); - const onKeypressHandler = (e: React.KeyboardEvent) => { + const onKeypressHandler = (e: KeyboardEvent) => { if (e.key === 'Enter') { if (validate(draftValue)) { // If the field is valid blur which will trigger validation a store diff --git a/app/scripts/components/common/stressed-form-group-input.tsx b/app/scripts/components/common/stressed-form-group-input.tsx index 2deb426c6..ca87a2f62 100644 --- a/app/scripts/components/common/stressed-form-group-input.tsx +++ b/app/scripts/components/common/stressed-form-group-input.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { Dispatch } from 'react'; import { FormInput, FormGroupStructure, @@ -16,7 +16,7 @@ interface StressedFormGroupInputProps { inputSize?: formSkinStylesProps['size']; placeholder?: number | string; validate: (value: string) => boolean; - onChange: (draftValue: string, setDraftValue: React.Dispatch) => void; + onChange: (draftValue: string, setDraftValue: Dispatch) => void; helper?: JSX.Element | null; hideHeader: boolean; } diff --git a/app/scripts/components/common/text-highlight.tsx b/app/scripts/components/common/text-highlight.tsx new file mode 100644 index 000000000..4ff5e4c2d --- /dev/null +++ b/app/scripts/components/common/text-highlight.tsx @@ -0,0 +1,50 @@ +import React, { ComponentType, ReactNode } from 'react'; +import styled from 'styled-components'; +import { themeVal } from '@devseed-ui/theme-provider'; + +const SearchHighlight = styled.mark` + font-style: italic; + background-color: ${themeVal('color.warning')}; +`; + +interface TextHighlightProps { + children: string; + value?: string; + highlightEl?: ComponentType | keyof JSX.IntrinsicElements; + disabled?: boolean; +} + +export default function TextHighlight(props: TextHighlightProps) { + const { children, value, highlightEl, disabled } = props; + + if (!value || disabled) return <>{children}; + const El = highlightEl ?? SearchHighlight; + + // Highlight is done index based because it has to take case insensitive + // searches into account. + const regex = new RegExp(value, 'ig'); + /* eslint-disable-next-line prefer-const */ + let highlighted: ReactNode[] = []; + let workingIdx = 0; + let m; + /* eslint-disable-next-line no-cond-assign */ + while ((m = regex.exec(children)) !== null) { + // Prevent infinite loops with zero-width matches. + if (m.index === regex.lastIndex) regex.lastIndex++; + + // Store string since last match. + highlighted = highlighted.concat(children.substring(workingIdx, m.index)); + // Highlight word. + highlighted = highlighted.concat( + + {children.substring(m.index, m.index + value.length)} + + ); + // Move index forward. + workingIdx = m.index + value.length; + } + // Add last piece. From working index to the end. + highlighted = highlighted.concat(children.substring(workingIdx)); + + return <>{highlighted}; +} \ No newline at end of file diff --git a/app/scripts/components/common/try-render.js b/app/scripts/components/common/try-render.js index b1e4b15e8..39e4d7aef 100644 --- a/app/scripts/components/common/try-render.js +++ b/app/scripts/components/common/try-render.js @@ -1,5 +1,5 @@ /* eslint-disable react/prop-types */ -import React from 'react'; +import React, { isValidElement } from 'react'; /** * Tries to render the given function falling back to the children if it is not @@ -16,14 +16,14 @@ export default function Try(props) { let value = children; - // Styled-components fail with React.isValidElement, so checking directly. - if (React.isValidElement(F) || F?.styledComponentId) { + // Styled-components fail with isValidElement, so checking directly. + if (isValidElement(F) || F?.styledComponentId) { value = ; } else if (typeof F === 'function') { value = F(rest); } - if (React.isValidElement(W) || W?.styledComponentId) { + if (isValidElement(W) || W?.styledComponentId) { return value ? {value} : null; } diff --git a/app/scripts/components/data-catalog/browse-controls.tsx b/app/scripts/components/data-catalog/browse-controls.tsx new file mode 100644 index 000000000..f49367404 --- /dev/null +++ b/app/scripts/components/data-catalog/browse-controls.tsx @@ -0,0 +1,234 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Overline } from '@devseed-ui/typography'; +import { Button, ButtonProps } from '@devseed-ui/button'; +import { + CollecticonChevronDownSmall, + CollecticonChevronUpSmall +} from '@devseed-ui/collecticons'; +import { glsp, media, truncated } from '@devseed-ui/theme-provider'; +import { DropMenu, DropTitle } from '@devseed-ui/dropdown'; +import { ShadowScrollbar } from '@devseed-ui/shadow-scrollbar'; + +import { + Actions, + FilterOption, + sortDirOptions, + sortFieldsOptions, + useBrowserControls +} from './use-browse-controls'; + +import DropdownScrollable from '$components/common/dropdown-scrollable'; +import DropMenuItemButton from '$styles/drop-menu-item-button'; +import { variableGlsp } from '$styles/variable-utils'; +import { FoldHeadActions } from '$components/common/fold'; +import SearchField from '$components/common/search-field'; +import { useMediaQuery } from '$utils/use-media-query'; + +const BrowseControlsWrapper = styled(FoldHeadActions)` + .search-field { + order: -1; + } + + ${media.largeUp` + .search-field { + order: initial; + } + `} +`; + +const BrowseControlsShadowScrollbar = styled(ShadowScrollbar)` + min-width: 0; +`; + +const DropButton = styled(Button)` + max-width: 14rem; + + > span { + ${truncated()} + } + + > svg { + flex-shrink: 0; + } +`; + +const ButtonPrefix = styled(Overline).attrs({ as: 'small' })` + margin-right: ${glsp(0.25)}; + white-space: nowrap; +`; + +const ShadowScrollbarInner = styled.div` + display: flex; + flex-flow: row nowrap; + gap: ${variableGlsp(0.5)}; + + > * { + flex-shrink: 0; + } +`; + +interface BrowseControlsProps extends ReturnType { + topicsOptions: FilterOption[]; + sourcesOptions: FilterOption[]; +} + +const shadowScrollbarProps = { + autoHeight: true +}; + +function BrowseControls(props: BrowseControlsProps) { + const { + topic, + source, + topicsOptions, + sourcesOptions, + search, + sortField, + sortDir, + onAction, + ...rest + } = props; + + const currentSortField = sortFieldsOptions.find((s) => s.id === sortField)!; + + const { isLargeUp } = useMediaQuery(); + + return ( + + + + onAction(Actions.TOPIC, v)} + /> + onAction(Actions.SOURCE, v)} + /> + onAction(Actions.SEARCH, v)} + /> + ( + + Sort by + {currentSortField.name}{' '} + {active ? ( + + ) : ( + + )} + + )} + > + Options + + {sortFieldsOptions.map((t) => ( +
  • + onAction(Actions.SORT_FIELD, t.id)} + > + {t.name} + +
  • + ))} +
    + + {sortDirOptions.map((t) => ( +
  • + onAction(Actions.SORT_DIR, t.id)} + > + {t.name} + +
  • + ))} +
    +
    +
    +
    +
    + ); +} + +export default styled(BrowseControls)` +/* Convert to styled-component: https://styled-components.com/docs/advanced#caveat */ +`; + +interface DropdownOptionsProps { + size: ButtonProps['size']; + items: FilterOption[]; + currentId: string | null; + onChange: (value: FilterOption['id']) => void; + prefix: string; +} + +function DropdownOptions(props: DropdownOptionsProps) { + const { size, items, currentId, onChange, prefix } = props; + + const currentItem = items.find((d) => d.id === currentId); + + return ( + ( + + {prefix} + {currentItem?.name}{' '} + {active ? ( + + ) : ( + + )} + + )} + > + Options + + {items.map((t) => ( +
  • + onChange(t.id)} + > + {t.name} + +
  • + ))} +
    +
    + ); +} diff --git a/app/scripts/components/data-catalog/dataset-menu.tsx b/app/scripts/components/data-catalog/dataset-menu.tsx new file mode 100644 index 000000000..c13df0747 --- /dev/null +++ b/app/scripts/components/data-catalog/dataset-menu.tsx @@ -0,0 +1,106 @@ +import React, { useCallback, useState } from 'react'; +import styled from 'styled-components'; +import { visuallyDisabled } from '@devseed-ui/theme-provider'; +import { Link } from 'react-router-dom'; +import { DatasetData } from 'veda'; +import { + DropMenu, + Dropdown, + DropMenuItem, + DropTitle +} from '@devseed-ui/dropdown'; +import { + CollecticonCode, + CollecticonCompass, + CollecticonEllipsisVertical, + CollecticonPage +} from '@devseed-ui/collecticons'; +import { Button } from '@devseed-ui/button'; + +import { getDatasetPath, getDatasetExplorePath } from '$utils/routes'; +import { NotebookConnectModal } from '$components/common/notebook-connect'; +import DropMenuItemButton from '$styles/drop-menu-item-button'; +import { Tip } from '$components/common/tip'; + +const DropMenuItemButtonDisable = styled(DropMenuItemButton)<{ + visuallyDisabled: boolean; +}>` + ${({ visuallyDisabled: v }) => v && visuallyDisabled()} +`; + +interface DatasetMenuProps { + dataset: DatasetData; +} + +function DatasetMenu(props: DatasetMenuProps) { + const { dataset } = props; + + const [revealed, setRevealed] = useState(false); + const close = useCallback(() => setRevealed(false), []); + + const hasUsage = !!dataset.usage?.length; + + return ( + <> + + ( + + )} + > + Options + +
  • + + Learn more + +
  • +
  • + + Explore data + +
  • +
  • + + setRevealed(true)} + visuallyDisabled={!hasUsage} + > + + Analyze data (Python) + + +
  • +
    +
    + + ); +} + +export default DatasetMenu; diff --git a/app/scripts/components/data-catalog/featured-datasets.tsx b/app/scripts/components/data-catalog/featured-datasets.tsx new file mode 100644 index 000000000..4fe4a5726 --- /dev/null +++ b/app/scripts/components/data-catalog/featured-datasets.tsx @@ -0,0 +1,112 @@ +import React from 'react'; +import styled from 'styled-components'; +import { datasets } from 'veda'; + +import DatasetMenu from './dataset-menu'; + +import { Card, CardMeta, CardTopicsList } from '$components/common/card'; +import { FoldGrid, FoldHeader, FoldTitle } from '$components/common/fold'; +import { + Continuum, + ContinuumGridItem, + ContinuumCardsDragScrollWrapper, + ContinuumDragScroll +} from '$styles/continuum'; +import { useReactIndianaScrollControl } from '$styles/continuum/use-react-indiana-scroll-controls'; +import { ContinuumScrollIndicator } from '$styles/continuum/continuum-scroll-indicator'; +import { getDatasetPath } from '$utils/routes'; +import { Pill } from '$styles/pill'; + +const allFeaturedDatasets = Object.values(datasets) + .map((d) => d!.data) + .filter((d) => d.featured); + +const FoldFeatured = styled(FoldGrid)` + ${FoldHeader} { + grid-column: content-start / content-end; + } +`; + +export const continuumFoldStartCols = { + smallUp: 'content-start', + mediumUp: 'content-start', + largeUp: 'content-start' +}; + +export const continuumFoldSpanCols = { + smallUp: 3, + mediumUp: 5, + largeUp: 8 +}; + +function FeaturedDatasets() { + const { isScrolling, scrollProps } = useReactIndianaScrollControl(); + + return ( + + + Featured Datasets + + + + + { + return allFeaturedDatasets.map((d) => ( + + { + // If the user was scrolling and let go of the mouse on top of a + // card a click event is triggered. We capture the click on the + // parent and never let it reach the link. + if (isScrolling) { + e.preventDefault(); + } + }} + cardType='featured' + linkLabel='View more' + linkTo={getDatasetPath(d)} + title={d.name} + overline={ + + By SOURCE + {/* TODO: Implement modified date: https://github.com/NASA-IMPACT/veda-ui/issues/514 */} + {/* + + Updated + */} + + } + description={d.description} + imgSrc={d.media?.src} + imgAlt={d.media?.alt} + footerContent={ + <> + {d.thematics?.length ? ( + +
    Topics
    + {d.thematics.map((t) => ( +
    + {t} +
    + ))} + + ) : null} + + + } + /> + + )); + }} + /> + + + + ); +} + +export default FeaturedDatasets; diff --git a/app/scripts/components/data-catalog/index.tsx b/app/scripts/components/data-catalog/index.tsx index b4853ee50..5ef95e15d 100644 --- a/app/scripts/components/data-catalog/index.tsx +++ b/app/scripts/components/data-catalog/index.tsx @@ -1,22 +1,149 @@ -import React from 'react'; -import { datasets } from 'veda'; +import React, { useMemo, useRef } from 'react'; +import styled from 'styled-components'; +import { DatasetData, datasets } from 'veda'; +import { Link } from 'react-router-dom'; +import { glsp, media } from '@devseed-ui/theme-provider'; +import { Subtitle } from '@devseed-ui/typography'; +import { Button } from '@devseed-ui/button'; +import { CollecticonXmarkSmall } from '@devseed-ui/collecticons'; -import { LayoutProps } from '$components/common/layout-root'; +import BrowseControls from './browse-controls'; +import { Actions, optionAll, useBrowserControls } from './use-browse-controls'; +import FeaturedDatasets from './featured-datasets'; +import DatasetMenu from './dataset-menu'; + +import { + LayoutProps, + useSlidingStickyHeaderProps +} from '$components/common/layout-root'; import PageHero from '$components/common/page-hero'; -import { Fold, FoldHeader, FoldTitle } from '$components/common/fold'; -import { Card, CardList } from '$components/common/card'; +import { + Fold, + FoldHeader, + FoldHeadline, + FoldTitle +} from '$components/common/fold'; +import { + Card, + CardList, + CardMeta, + CardTopicsList +} from '$components/common/card'; import EmptyHub from '$components/common/empty-hub'; - import { PageMainContent } from '$styles/page'; import { DATASETS_PATH, getDatasetPath } from '$utils/routes'; +import TextHighlight from '$components/common/text-highlight'; +import Pluralize from '$utils/pluralize'; +import { Pill } from '$styles/pill'; + +const allDatasets = Object.values(datasets).map((d) => d!.data); + +const DatasetCount = styled(Subtitle)` + grid-column: 1 / -1; + display: flex; + gap: ${glsp(0.5)}; + + span { + text-transform: uppercase; + line-height: 1.5rem; + } +`; + +const BrowseHeader = styled(FoldHeader)` + ${media.largeUp` + ${FoldHeadline} { + align-self: flex-start; + } + + ${BrowseControls} { + padding-top: 1rem; + } + `} +`; + +const topicsOptions = [ + optionAll, + // TODO: human readable values for Taxonomies + ...Array.from(new Set(allDatasets.flatMap((d) => d.thematics || []))).map( + (t) => ({ + id: t, + name: t + }) + ) +]; + +const sourcesOptions = [optionAll]; + +const prepareDatasets = (data: DatasetData[], options) => { + const { sortField, sortDir, search, topic, source } = options; + + let filtered = [...data]; + + // Does the free text search appear in specific fields? + if (search.length >= 3) { + const searchLower = search.toLowerCase(); + filtered = filtered.filter( + (d) => + d.name.toLowerCase().includes(searchLower) || + d.description.toLowerCase().includes(searchLower) || + d.layers.some((l) => l.stacCol.toLowerCase().includes(searchLower)) || + d.thematics?.some((t) => t.toLowerCase().includes(searchLower)) + ); + } + + if (topic !== optionAll.id) { + filtered = filtered.filter((d) => d.thematics?.includes(topic)); + } + + if (source !== optionAll.id) { + // TODO: Filter source + } -/* eslint-disable-next-line fp/no-mutating-methods */ -const allDatasets = Object.values(datasets) - .filter((d) => !!d?.data) - .map((d) => d!.data) - .sort((a, b) => a.name.localeCompare(b.name)); + /* eslint-disable-next-line fp/no-mutating-methods */ + filtered.sort((a, b) => { + if (!a[sortField]) return Infinity; + + return a[sortField]?.localeCompare(b[sortField]); + }); + + if (sortDir === 'desc') { + /* eslint-disable-next-line fp/no-mutating-methods */ + filtered.reverse(); + } + + return filtered; +}; function DataCatalog() { + const controlVars = useBrowserControls({ + topicsOptions, + sourcesOptions + }); + + const { topic, source, sortField, sortDir, onAction } = controlVars; + const search = controlVars.search ?? ''; + + const displayDatasets = useMemo( + () => + prepareDatasets(allDatasets, { + search, + topic, + source, + sortField, + sortDir + }), + [search, topic, source, sortField, sortDir] + ); + + const isFiltering = !!( + topic !== optionAll.id || + source !== optionAll.id || + search + ); + + const browseControlsHeaderRef = useRef(null); + const { headerHeight } = useSlidingStickyHeaderProps(); + return ( + + + - - Browse - - {allDatasets.length ? ( + + + Browse + + + + + + + Showing{' '} + {' '} + out of {allDatasets.length}. + + {isFiltering && ( + + )} + + + {displayDatasets.length ? ( - {allDatasets.map((t) => ( -
  • + {displayDatasets.map((d) => ( +
  • + { + e.preventDefault(); + onAction(Actions.SOURCE, 'eis'); + browseControlsHeaderRef.current?.scrollIntoView(); + }} + > + By SOURCE + + {/* TODO: Implement modified date: https://github.com/NASA-IMPACT/veda-ui/issues/514 */} + {/* + { + e.preventDefault(); + onAction(Actions.SORT_FIELD, 'date'); + }} + > + Updated + */} + + } linkLabel='View more' - linkTo={getDatasetPath(t)} - title={t.name} - parentName='Dataset' - parentTo={DATASETS_PATH} - description={t.description} - imgSrc={t.media?.src} - imgAlt={t.media?.alt} + linkTo={getDatasetPath(d)} + title={ + + {d.name} + + } + description={ + + {d.description} + + } + imgSrc={d.media?.src} + imgAlt={d.media?.alt} + footerContent={ + <> + {d.thematics?.length ? ( + +
    Topics
    + {d.thematics.map((t) => ( +
    + { + e.preventDefault(); + onAction(Actions.TOPIC, t); + browseControlsHeaderRef.current?.scrollIntoView(); + }} + > + + {t} + + +
    + ))} +
    + ) : null} + + + } />
  • ))}
    ) : ( - There are no datasets to show. Check back later. + + There are no datasets to show with the selected filters. + )}
    diff --git a/app/scripts/components/data-catalog/use-browse-controls.ts b/app/scripts/components/data-catalog/use-browse-controls.ts new file mode 100644 index 000000000..f5113a423 --- /dev/null +++ b/app/scripts/components/data-catalog/use-browse-controls.ts @@ -0,0 +1,139 @@ +import { useCallback } from 'react'; +import { useNavigate } from 'react-router'; +import useQsStateCreator from 'qs-state-hook'; + +export enum Actions { + SEARCH = 'search', + SORT_FIELD = 'sfield', + SORT_DIR = 'sdir', + TOPIC = 'topic', + SOURCE = 'source' +} + +export type BrowserControlsAction = (what: Actions, value: any) => void; + +export interface FilterOption { + id: string; + name: string; +} + +interface BrowseControlsHookParams { + topicsOptions: FilterOption[]; + sourcesOptions: FilterOption[]; +} + +export const sortFieldsOptions: FilterOption[] = [ + { + id: 'name', + name: 'Name' + } + // TODO: Implement date sorting: https://github.com/NASA-IMPACT/veda-ui/issues/514 + // { + // id: 'date', + // name: 'Date (n/a)' + // } +]; + +export const sortDirOptions: FilterOption[] = [ + { + id: 'asc', + name: 'Ascending' + }, + { + id: 'desc', + name: 'Descending' + } +]; + +export const optionAll = { + id: 'all', + name: 'All' +}; + +export function useBrowserControls({ + topicsOptions, + sourcesOptions +}: BrowseControlsHookParams) { + // Setup Qs State to store data in the url's query string + // react-router function to get the navigation. + const navigate = useNavigate(); + const useQsState = useQsStateCreator({ + commit: navigate + }); + + const [sortField, setSortField] = useQsState.memo( + { + key: Actions.SORT_FIELD, + default: sortFieldsOptions[0].id, + validator: sortFieldsOptions.map((d) => d.id) + }, + [] + ); + + const [sortDir, setSortDir] = useQsState.memo( + { + key: Actions.SORT_DIR, + default: sortDirOptions[0].id, + validator: sortDirOptions.map((d) => d.id) + }, + [] + ); + + const [search, setSearch] = useQsState.memo( + { + key: Actions.SEARCH, + default: '' + }, + [] + ); + + const [topic, setTopic] = useQsState.memo( + { + key: Actions.TOPIC, + default: topicsOptions[0].id, + validator: topicsOptions.map((d) => d.id) + }, + [topicsOptions] + ); + + const [source, setSource] = useQsState.memo( + { + key: Actions.SOURCE, + default: sourcesOptions[0].id, + validator: sourcesOptions.map((d) => d.id) + }, + [sourcesOptions] + ); + + const onAction = useCallback( + (what, value) => { + switch (what) { + case Actions.SEARCH: + setSearch(value); + break; + case Actions.SORT_FIELD: + setSortField(value); + break; + case Actions.SORT_DIR: + setSortDir(value); + break; + case Actions.TOPIC: + setTopic(value); + break; + case Actions.SOURCE: + setSource(value); + break; + } + }, + [setSortField, setSortDir, setTopic, setSource, setSearch] + ); + + return { + search, + sortField, + sortDir, + topic, + source, + onAction + }; +} diff --git a/app/scripts/components/datasets/s-explore/index.tsx b/app/scripts/components/datasets/s-explore/index.tsx index a89f34108..9b63e8c29 100644 --- a/app/scripts/components/datasets/s-explore/index.tsx +++ b/app/scripts/components/datasets/s-explore/index.tsx @@ -558,7 +558,7 @@ function DatasetsExplore() { - + { @@ -102,7 +102,7 @@ export function PanelDateWidget(props: PanelDateWidgetProps) { {!!availableDates && ( - + { @@ -153,7 +153,7 @@ export function PanelDateWidget(props: PanelDateWidgetProps) { ); }} /> - +
    )} diff --git a/app/scripts/components/datasets/s-overview/index.tsx b/app/scripts/components/datasets/s-overview/index.tsx index a10e2c272..385434c73 100644 --- a/app/scripts/components/datasets/s-overview/index.tsx +++ b/app/scripts/components/datasets/s-overview/index.tsx @@ -54,7 +54,7 @@ function DatasetsOverview() { Explore data diff --git a/app/scripts/styles/browser-frame.tsx b/app/scripts/styles/browser-frame.tsx index 0b77de4f7..048abb7b7 100644 --- a/app/scripts/styles/browser-frame.tsx +++ b/app/scripts/styles/browser-frame.tsx @@ -1,8 +1,8 @@ -import React from 'react'; +import React, { ReactNode } from 'react'; import styled from 'styled-components'; import { themeVal } from '@devseed-ui/theme-provider'; -function BrowserFrameComponent(props: { children: React.ReactNode }) { +function BrowserFrameComponent(props: { children: ReactNode }) { const { children, ...rest } = props; return (
    diff --git a/app/scripts/styles/continuum/index.tsx b/app/scripts/styles/continuum/index.tsx index 743136ca7..1fa099c59 100644 --- a/app/scripts/styles/continuum/index.tsx +++ b/app/scripts/styles/continuum/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { ReactNode, useEffect } from 'react'; import styled from 'styled-components'; import useDimensions from 'react-cool-dimensions'; import DragScroll from 'react-indiana-drag-scroll'; @@ -17,7 +17,7 @@ interface ContinuumRenderFunctionBag { interface ContinuumProps { className?: string; - render?: (bag: ContinuumRenderFunctionBag) => React.ReactNode; + render?: (bag: ContinuumRenderFunctionBag) => ReactNode; startCol: { largeUp: string; mediumUp: string; @@ -28,7 +28,7 @@ interface ContinuumProps { mediumUp: number; smallUp: number; }; - children?: React.ReactNode; + children?: ReactNode; listAs?: any; } diff --git a/app/scripts/styles/drop-menu-item-button.tsx b/app/scripts/styles/drop-menu-item-button.tsx index 9f43bd8e7..0dfe18166 100644 --- a/app/scripts/styles/drop-menu-item-button.tsx +++ b/app/scripts/styles/drop-menu-item-button.tsx @@ -1,5 +1,8 @@ -import styled from 'styled-components'; +import styled, { css } from 'styled-components'; import { DropMenuItem } from '@devseed-ui/dropdown'; +import { rgba, themeVal } from '@devseed-ui/theme-provider'; + +const rgbaFixed = rgba as any; const DropMenuItemButton = styled(DropMenuItem).attrs({ as: 'button', @@ -9,6 +12,16 @@ const DropMenuItemButton = styled(DropMenuItem).attrs({ border: none; width: 100%; cursor: pointer; + text-align: left; + + ${({ active }) => + active && + css` + &, + &:visited { + background-color: ${rgbaFixed(themeVal('color.link'), 0.08)}; + } + `} `; export default DropMenuItemButton; diff --git a/app/scripts/styles/pill.tsx b/app/scripts/styles/pill.tsx new file mode 100644 index 000000000..959124b6c --- /dev/null +++ b/app/scripts/styles/pill.tsx @@ -0,0 +1,29 @@ +import styled from 'styled-components'; +import { glsp, themeVal } from '@devseed-ui/theme-provider'; + +export const Pill = styled.span` + display: inline-flex; + vertical-align: top; + color: ${themeVal('color.surface')}; + border-radius: ${themeVal('shape.ellipsoid')}; + padding: ${glsp(0.125, 0.75)}; + background: ${themeVal('color.surface-100a')}; + transition: all 0.24s ease 0s; + font-size: 0.75rem; + line-height: 1.25rem; + font-weight: ${themeVal('type.base.bold')}; + white-space: nowrap; + + :is(a) { + pointer-events: auto; + + &, + &:visited { + text-decoration: none; + } + + &:hover { + opacity: 0.64; + } + } +`; diff --git a/app/scripts/utils/hinted-error.tsx b/app/scripts/utils/hinted-error.tsx index 7b0b14a2e..de800025a 100644 --- a/app/scripts/utils/hinted-error.tsx +++ b/app/scripts/utils/hinted-error.tsx @@ -1,11 +1,11 @@ -import React from 'react'; +import React, { Fragment, ReactNode } from 'react'; import styled from 'styled-components'; import { themeVal, glsp } from '@devseed-ui/theme-provider'; export class HintedError extends Error { - hints?: React.ReactNode[]; + hints?: ReactNode[]; - constructor(message, hints: React.ReactNode[] = []) { + constructor(message, hints: ReactNode[] = []) { super(message); this.hints = hints; } @@ -50,8 +50,8 @@ interface HintedErrorDisplayProps { title: string; message: string; className?: string; - hints?: React.ReactNode[]; - subtitle?: React.ReactNode; + hints?: ReactNode[]; + subtitle?: ReactNode; } export function HintedErrorDisplay(props: HintedErrorDisplayProps) { @@ -75,7 +75,7 @@ export function HintedErrorDisplay(props: HintedErrorDisplayProps) {

    {e}

    ) : ( /* eslint-disable-next-line react/no-array-index-key */ - {e} + {e} ) )} diff --git a/app/scripts/utils/use-effect-previous.ts b/app/scripts/utils/use-effect-previous.ts index 52fbd8b85..10ca3d6ec 100644 --- a/app/scripts/utils/use-effect-previous.ts +++ b/app/scripts/utils/use-effect-previous.ts @@ -1,6 +1,9 @@ -import React, { useEffect, useRef } from 'react'; +import { DependencyList, useEffect, useRef } from 'react'; -type EffectPreviousCb = (previous: T, mounted: boolean) => void | (() => void) +type EffectPreviousCb = ( + previous: T, + mounted: boolean +) => void | (() => void); /** * Same behavior as React's useEffect but called with the values for the @@ -9,8 +12,11 @@ type EffectPreviousCb = (previous: T, mounted: boolean) => void | (() => void * @param {func} cb Hook callback * @param {array} deps Hook dependencies. */ -export function useEffectPrevious(cb: EffectPreviousCb , deps: T) { - const prev = useRef([]); +export function useEffectPrevious( + cb: EffectPreviousCb, + deps: T +) { + const prev = useRef([]); const mounted = useRef(false); const unchangingCb = useRef>(cb); unchangingCb.current = cb; diff --git a/app/scripts/utils/use-safe-state.ts b/app/scripts/utils/use-safe-state.ts index 4f7a4a266..4a6337ee8 100644 --- a/app/scripts/utils/use-safe-state.ts +++ b/app/scripts/utils/use-safe-state.ts @@ -1,11 +1,11 @@ -import React, { useCallback, useEffect, useRef, useState } from "react"; +import { Dispatch, useCallback, useEffect, useRef, useState } from 'react'; /** * React hook to set state. Same behavior as useState but won't set the state if * the component is unmounted. * @param {*} initialValue Initial state value. */ - export const useSafeState = (initialValue: T): [T, React.Dispatch] => { +export const useSafeState = (initialValue: T): [T, Dispatch] => { const isMountedRef = useRef(true); const [currentValue, setCurrentValue] = useState(initialValue); @@ -16,10 +16,10 @@ import React, { useCallback, useEffect, useRef, useState } from "react"; }, [isMountedRef]); const setSafeState = useCallback((value) => { - if (isMountedRef && isMountedRef.current) { + if (isMountedRef.current) { setCurrentValue(value); } }, []); return [currentValue, setSafeState]; -}; \ No newline at end of file +}; diff --git a/mock/datasets/sandbox.data.mdx b/mock/datasets/sandbox.data.mdx index d3ad5d1cc..ebb9fcc6a 100644 --- a/mock/datasets/sandbox.data.mdx +++ b/mock/datasets/sandbox.data.mdx @@ -12,6 +12,9 @@ media: thematics: - agriculture - covid-19 + - our-planet + - experimental + - untested layers: - id: blue-tarp-planetscope