diff --git a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx index 2332400a7..c2abe2a19 100644 --- a/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx +++ b/app/scripts/components/exploration/components/datasets/dataset-list-item.tsx @@ -211,6 +211,7 @@ export function DatasetListItem(props: DatasetListItemProps) { onDragEnd={() => { onDragEnd?.(); }} + data-tour='dataset-list-item' > diff --git a/app/scripts/components/exploration/components/map/analysis-message-control.tsx b/app/scripts/components/exploration/components/map/analysis-message-control.tsx index 119d48427..53c144938 100644 --- a/app/scripts/components/exploration/components/map/analysis-message-control.tsx +++ b/app/scripts/components/exploration/components/map/analysis-message-control.tsx @@ -22,7 +22,9 @@ import { useAnalysisController } from '$components/exploration/hooks/use-analysi import useThemedControl from '$components/common/map/controls/hooks/use-themed-control'; import { AoIFeature } from '$components/common/map/types'; -const AnalysisMessageWrapper = styled.div` +const AnalysisMessageWrapper = styled.div.attrs({ + 'data-tour': 'analysis-message' +})` background-color: ${themeVal('color.base-400a')}; color: ${themeVal('color.surface')}; border-radius: ${themeVal('shape.rounded')}; diff --git a/app/scripts/components/exploration/components/timeline/timeline-controls.tsx b/app/scripts/components/exploration/components/timeline/timeline-controls.tsx index c84219a34..db3130158 100644 --- a/app/scripts/components/exploration/components/timeline/timeline-controls.tsx +++ b/app/scripts/components/exploration/components/timeline/timeline-controls.tsx @@ -96,7 +96,7 @@ export function TimelineControls(props: TimelineControlsProps) { setSelectedDay(d.start!); }} renderTriggerElement={(props, label) => ( - + A {label} @@ -164,7 +164,7 @@ export function TimelineControls(props: TimelineControlsProps) { )} {selectedInterval && ( - + void; children: React.ReactNode; + 'data-tour'?: string; } type TimelineHeadProps = Omit & { @@ -84,7 +85,10 @@ export function TimelineHeadBase(props: TimelineHeadBaseProps) { return ( - + {children} diff --git a/app/scripts/components/exploration/components/timeline/timeline.tsx b/app/scripts/components/exploration/components/timeline/timeline.tsx index a250d8e32..383fd24a2 100644 --- a/app/scripts/components/exploration/components/timeline/timeline.tsx +++ b/app/scripts/components/exploration/components/timeline/timeline.tsx @@ -422,6 +422,7 @@ export default function Timeline(props: TimelineProps) { @@ -448,6 +449,7 @@ export default function Timeline(props: TimelineProps) { <> {selectedDay && ( ({ + ...base, + padding: '0', + background: 'none', + }) +}; + function Exploration() { const datasets = useAtomValue(timelineDatasetsAtom); const [datasetModalRevealed, setDatasetModalRevealed] = useState( @@ -66,12 +76,17 @@ function Exploration() { const closeModal = useCallback(() => setDatasetModalRevealed(false), []); return ( - <> + + @@ -97,7 +112,7 @@ function Exploration() { close={closeModal} /> - + ); } export default Exploration; diff --git a/app/scripts/components/exploration/tour-manager.tsx b/app/scripts/components/exploration/tour-manager.tsx new file mode 100644 index 000000000..a90858357 --- /dev/null +++ b/app/scripts/components/exploration/tour-manager.tsx @@ -0,0 +1,245 @@ +import React, { useCallback, useEffect, useState } from 'react'; +import { useTour, PopoverContentProps, StepType } from '@reactour/tour'; +import { useAtomValue } from 'jotai'; +import styled from 'styled-components'; +import { glsp, themeVal } from '@devseed-ui/theme-provider'; +import { Button } from '@devseed-ui/button'; +import { Heading } from '@devseed-ui/typography'; +import { + CollecticonChevronLeftSmall, + CollecticonChevronRightSmall, + CollecticonXmark +} from '@devseed-ui/collecticons'; + +import { timelineDatasetsAtom } from './atoms/datasets'; + +import { usePreviousValue } from '$utils/use-effect-previous'; +import useAois from '$components/common/map/controls/hooks/use-aois'; + +const Popover = styled.div` + position: relative; + background: ${themeVal('color.surface')}; + padding: ${glsp(1, 2, 1, 2)}; + border-radius: ${themeVal('shape.rounded')}; + display: flex; + flex-direction: column; + gap: ${glsp()}; +`; + +const CloseButton = styled(Button)` + position: absolute; + right: ${glsp(0.5)}; + top: ${glsp(0.5)}; +`; + +const PopoverBody = styled.div``; + +const PopoverFooter = styled.div` + display: flex; + justify-content: center; + align-items: center; + gap: ${glsp()}; + font-weight: ${themeVal('type.base.bold')}; +`; + +const introTourSteps = [ + { + title: 'Map layer selection', + selector: "[data-tour='dataset-list-item']", + mutationObservables: ["[data-tour='dataset-list-item']"], + content: + "Each row represents a dataset, and each of the boxes on the timeline represents a data unit: day, month or year, depending on the dataset's time density." + }, + { + title: 'Playhead', + selector: "[data-tour='timeline-head-a']", + content: + 'Move this playhead to select a date to view on the map. You can drag it around or click on the timeline to place it.' + }, + { + title: 'Date picker', + selector: "[data-tour='date-picker-a']", + content: 'Alternatively you can also select a date through the date picker.' + }, + { + title: 'Timeline', + selector: "[data-tour='timeline-interaction-rect']", + content: () => ( + <> + You can zoom in on the timeline by scrolling while pressing the alt key + (or option) and click and drag to pan. +
+ Go ahead and try it out! + + ) + }, + { + title: 'AOI tools', + selector: '.mapboxgl-ctrl-top-left', + content: () => ( + <> + You can calculate a time series of zonal statistics for your area of + interest (AOI). Start that process here by drawing or uploading an AOI. + + ), + stepInteraction: false + } +]; + +const analysisTourSteps = [ + { + title: 'Analysis', + selector: "[data-tour='analysis-message']", + content: () => ( + <> + You can now calculate a time series of zonal + statistics for your area of interest. + + ), + stepInteraction: false + }, + { + title: 'Date Range', + selector: "[data-tour='analysis-toolbar']", + content: () => ( + <> + Refine the date range to analyze with the data pickers + or handles on the timeline. +
+ Once you're happy, press the analyze button to start the calculation. + + ), + stepInteraction: false + } +]; + +/** + * Helper function to add an action after the last step of a tour. + * @param steps The steps to add the action to + * @param action The action to add to the last step + * @returns steps with the action added to the last step + */ +function addActionAfterLastStep(steps: StepType[], action: () => void) { + const lastStep = steps[steps.length - 1]; + const lastStepWithAction = { + ...lastStep, + actionAfter: action + }; + return [...steps.slice(0, -1), lastStepWithAction]; +} + +export function TourManager() { + const { setIsOpen, setSteps, setCurrentStep } = useTour(); + + const startTour = useCallback( + (steps) => { + setCurrentStep(0); + setSteps?.(steps); + setIsOpen(true); + }, + [setIsOpen, setSteps, setCurrentStep] + ); + + // Control states for the different tours. + const [introTourShown, setIntroTourShown] = useState(false); + const [analysisTourShown, setAnalysisTourShown] = useState(false); + + // Variables that cause tour 1 to start. + const datasets = useAtomValue(timelineDatasetsAtom); + const datasetCount = datasets.length; + const prevDatasetCount = usePreviousValue(datasetCount); + useEffect(() => { + if (!introTourShown && !prevDatasetCount && datasetCount > 0) { + // Make the last step of the intro tour mark it as shown. + const steps = addActionAfterLastStep(introTourSteps, () => { + setIntroTourShown(true); + }); + startTour(steps); + } + }, [introTourShown, prevDatasetCount, datasetCount, startTour]); + + // Variables that cause tour 2 to start. + const { features } = useAois(); + const featuresCount = features.length; + useEffect(() => { + if (introTourShown && !analysisTourShown && featuresCount > 0) { + // Make the last step of the intro tour mark it as shown. + const steps = addActionAfterLastStep(analysisTourSteps, () => { + setAnalysisTourShown(true); + }); + startTour(steps); + } + }, [introTourShown, analysisTourShown, featuresCount, startTour]); + + return null; +} + +interface ExtendedPopoverContentProps extends PopoverContentProps { + steps: (StepType & { title: string })[]; +} + +export function PopoverTourComponent(props: ExtendedPopoverContentProps) { + const { currentStep, steps, setIsOpen, setCurrentStep } = props; + + const isLastStep = currentStep === steps.length - 1; + const { content, title } = steps[currentStep]; + return ( + + setIsOpen(false)} + > + + + + {title} + + + <> + {/* Check if the step.content is a function or a string */} + {typeof content === 'function' ? content({ ...props }) : content} + + + + + + {currentStep + 1} / {steps.length} + + + + {/* {(currentStep === 0 || isLastStep) && ( + { + e.preventDefault(); + setIsOpen(false); + }} + > + Dismiss and do not show again + + )} */} + + ); +} diff --git a/package.json b/package.json index 5142fa132..e924b694e 100644 --- a/package.json +++ b/package.json @@ -113,6 +113,7 @@ "@mapbox/mapbox-gl-draw": "^1.3.0", "@mapbox/mapbox-gl-geocoder": "^5.0.1", "@parcel/transformer-raw": "~2.7.0", + "@reactour/tour": "^3.6.1", "@tanstack/react-query": "^4.3.9", "@tanstack/react-query-devtools": "^4.3.9", "@tanstack/react-table": "^8.9.3", diff --git a/yarn.lock b/yarn.lock index 25e0c8902..eb0f5ee5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2962,6 +2962,42 @@ resolved "http://verdaccio.ds.io:4873/@reach%2fobserve-rect/-/observe-rect-1.2.0.tgz#d7a6013b8aafcc64c778a0ccb83355a11204d3b2" integrity sha512-Ba7HmkFgfQxZqqaeIWWkNK0rEhpxVQHIoVyW1YDSkGsGIXzcaW4deC8B0pZrNSSyLTdIk7y+5olKt5+g0GmFIQ== +"@reactour/mask@*": + version "1.1.0" + resolved "http://verdaccio.ds.io:4873/@reactour%2fmask/-/mask-1.1.0.tgz#e327306585ee3510e80169a7fa811e9d0b9448bb" + integrity sha512-GkJMLuTs3vTsm4Ryq2uXcE4sMzRP1p4xSd6juSOMqbHa7IVD/UiLCLqJWHR9xGSQPbYhpZAZAORUG5cS0U5tBA== + dependencies: + "@reactour/utils" "*" + +"@reactour/popover@*": + version "1.1.1" + resolved "http://verdaccio.ds.io:4873/@reactour%2fpopover/-/popover-1.1.1.tgz#c9b05ee31b8677d874f1479d724204b936e1128f" + integrity sha512-BouulO0sXfmuHSPX8FwCYI0fMeT+VsWqk7UTao3NQcUC5H903ZeKOV2GYpwSJtRUQhsyNEu1Q8cEruGRf1SOXQ== + dependencies: + "@reactour/utils" "*" + +"@reactour/tour@^3.6.1": + version "3.6.1" + resolved "http://verdaccio.ds.io:4873/@reactour%2ftour/-/tour-3.6.1.tgz#7537c8faa48546fe4e312f125113459b7a19fa13" + integrity sha512-vzmgbm4T7n5gh0cjc4Zi4G3K29dXQyEdi/o7ZYLpNcisJ0hwP5jNKH7BgckrHWEGldBxYSWl34tsRmHcyxporQ== + dependencies: + "@reactour/mask" "*" + "@reactour/popover" "*" + "@reactour/utils" "*" + +"@reactour/utils@*": + version "0.5.0" + resolved "http://verdaccio.ds.io:4873/@reactour%2futils/-/utils-0.5.0.tgz#8886872d78839187fd53399834a1b9688e98d754" + integrity sha512-yQs5Nm/Dg1xRM7d/S/UILBV5OInrTgrjGzgc81/RP5khqdO5KnpOaC46yF83kDtCalte8X3RCwp+F2YA509k1w== + dependencies: + "@rooks/use-mutation-observer" "^4.11.2" + resize-observer-polyfill "^1.5.1" + +"@rooks/use-mutation-observer@^4.11.2": + version "4.11.2" + resolved "http://verdaccio.ds.io:4873/@rooks%2fuse-mutation-observer/-/use-mutation-observer-4.11.2.tgz#a0466c4338e0a4487ea19253c86bcd427c29f4af" + integrity sha512-vpsdrZdr6TkB1zZJcHx+fR1YC/pHs2BaqcuYiEGjBVbwY5xcC49+h0hAUtQKHth3oJqXfIX/Ng8S7s5HFHdM/A== + "@sinclair/typebox@^0.24.1": version "0.24.20" resolved "http://verdaccio.ds.io:4873/@sinclair%2ftypebox/-/typebox-0.24.20.tgz#11a657875de6008622d53f56e063a6347c51a6dd" @@ -11321,6 +11357,11 @@ require-main-filename@^1.0.1: resolved "http://verdaccio.ds.io:4873/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= +resize-observer-polyfill@^1.5.1: + version "1.5.1" + resolved "http://verdaccio.ds.io:4873/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" + integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== + resolve-cwd@^3.0.0: version "3.0.0" resolved "http://verdaccio.ds.io:4873/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d"