diff --git a/fableous-fe/package.json b/fableous-fe/package.json index 01867dd5..672f98ed 100644 --- a/fableous-fe/package.json +++ b/fableous-fe/package.json @@ -24,6 +24,7 @@ "react": "^17.0.2", "react-canvas-confetti": "^1.2.1", "react-dom": "^17.0.2", + "react-joyride": "^2.3.1", "react-resize-detector": "^6.7.6", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", diff --git a/fableous-fe/src/App.tsx b/fableous-fe/src/App.tsx index 1ced34f2..89753b94 100644 --- a/fableous-fe/src/App.tsx +++ b/fableous-fe/src/App.tsx @@ -12,6 +12,7 @@ import Navbar from "./components/Navbar"; import Routes from "./Routes"; import InjectAxiosRespInterceptor from "./components/InjectAxiosRespInterceptor"; import { colors } from "./colors"; +import CustomNavProvider from "./components/CustomNavProvider"; // generated background from https://www.svgbackgrounds.com/ const useStyles = makeStyles({ @@ -136,13 +137,15 @@ export default function App() { - - - - - - - + + + + + + + + + diff --git a/fableous-fe/src/api.ts b/fableous-fe/src/api.ts index 0499a1f7..47ff8403 100644 --- a/fableous-fe/src/api.ts +++ b/fableous-fe/src/api.ts @@ -1,6 +1,7 @@ import axios, { AxiosRequestConfig, AxiosResponse } from "axios"; import { configure } from "axios-hooks"; import { TOKEN_KEY } from "./components/AuthProvider"; +import { getLocalStorage } from "./storage"; import { proto as pb } from "./proto/message_pb"; const baseAPI = @@ -16,7 +17,7 @@ const baseWS = const apiClient = axios.create(); apiClient.defaults.baseURL = baseAPI; apiClient.interceptors.request.use((req: AxiosRequestConfig) => { - const token = localStorage.getItem(TOKEN_KEY); + const token = getLocalStorage(TOKEN_KEY); if (token) { req.headers = { authorization: `Bearer ${token}`, @@ -137,13 +138,12 @@ export const restAPI = { method: "delete", }), }, - // @TODO: create POST for story saving } as ApiEndpoints; export const wsAPI = { hub: { main: (classroomId: string) => { - const token = localStorage.getItem(TOKEN_KEY); + const token = getLocalStorage(TOKEN_KEY); return `${baseWS}/ws/hub?token=${token}&classroom_id=${classroomId}`; }, }, diff --git a/fableous-fe/src/components/AuthProvider.tsx b/fableous-fe/src/components/AuthProvider.tsx index 4a84e50c..73211f8e 100644 --- a/fableous-fe/src/components/AuthProvider.tsx +++ b/fableous-fe/src/components/AuthProvider.tsx @@ -1,4 +1,5 @@ import { createContext, ReactNode, useState } from "react"; +import { getLocalStorage, setLocalStorage } from "../storage"; export const TOKEN_KEY = "token"; @@ -17,7 +18,7 @@ export const AuthContext = createContext([ ]); export default function AuthProvider(props: { children: ReactNode }) { - const [token, setToken] = useState(localStorage.getItem(TOKEN_KEY) || ""); + const [token, setToken] = useState(getLocalStorage(TOKEN_KEY) || ""); const { children } = props; return ( @@ -26,11 +27,11 @@ export default function AuthProvider(props: { children: ReactNode }) { token, token !== "", (t: string) => { - localStorage.setItem(TOKEN_KEY, t); + setLocalStorage(TOKEN_KEY, t); setToken(t); }, () => { - localStorage.setItem(TOKEN_KEY, ""); + setLocalStorage(TOKEN_KEY, ""); setToken(""); }, ]} diff --git a/fableous-fe/src/components/ChipRow.tsx b/fableous-fe/src/components/ChipRow.tsx index 67758515..841ccbd4 100644 --- a/fableous-fe/src/components/ChipRow.tsx +++ b/fableous-fe/src/components/ChipRow.tsx @@ -5,13 +5,20 @@ import React from "react"; interface ChipRowProps { chips: (React.ReactNode | ChipProps | string)[]; primary?: boolean; + rootProps?: React.DetailedHTMLProps< + React.HTMLAttributes, + HTMLDivElement + >; } export default function ChipRow(props: ChipRowProps) { - const { chips, primary } = props; + const { chips, primary, rootProps } = props; return ( -
+
{chips.map((chip) => React.isValidElement(chip) ? ( chip @@ -30,4 +37,5 @@ export default function ChipRow(props: ChipRowProps) { ChipRow.defaultProps = { primary: false, + rootProps: {}, }; diff --git a/fableous-fe/src/components/CustomNavProvider.tsx b/fableous-fe/src/components/CustomNavProvider.tsx new file mode 100644 index 00000000..84881d08 --- /dev/null +++ b/fableous-fe/src/components/CustomNavProvider.tsx @@ -0,0 +1,43 @@ +import { ButtonProps } from "@material-ui/core"; +import { createContext, ReactNode, useContext, useState } from "react"; + +export interface Nav { + icon: string; + label: string; + buttonProps: ButtonProps; +} + +export type CustomNavContextType = [ + additionalNavs: Nav[], + setAdditionalNavs: React.Dispatch>, + isLogoClickable: boolean, + setIsLogoClickable: React.Dispatch> +]; + +export const CustomNavContext = createContext([ + [], + (_) => {}, + true, + (_) => {}, +]); + +export const useCustomNav = () => useContext(CustomNavContext); + +export default function CustomNavProvider(props: { children: ReactNode }) { + const [additionalNavs, setAdditionalNavs] = useState([]); + const [isLogoClickable, setIsLogoClickable] = useState(true); + const { children } = props; + + return ( + + {children} + + ); +} diff --git a/fableous-fe/src/components/Navbar.tsx b/fableous-fe/src/components/Navbar.tsx index fe95483b..ecc8339a 100644 --- a/fableous-fe/src/components/Navbar.tsx +++ b/fableous-fe/src/components/Navbar.tsx @@ -1,12 +1,13 @@ +import { ReactNode, useContext } from "react"; import { Icon, makeStyles } from "@material-ui/core"; import AppBar from "@material-ui/core/AppBar"; import Toolbar from "@material-ui/core/Toolbar"; import Typography from "@material-ui/core/Typography"; import Button from "@material-ui/core/Button"; -import { Link, useHistory } from "react-router-dom"; -import { useContext } from "react"; +import { Link, useHistory, useLocation } from "react-router-dom"; import { AuthContext } from "./AuthProvider"; +import { useCustomNav } from "./CustomNavProvider"; const useStyles = makeStyles(() => ({ home: { @@ -16,23 +17,44 @@ const useStyles = makeStyles(() => ({ export default function Navbar() { const history = useHistory(); + const location = useLocation(); const [, isAuthenticated, , clearToken] = useContext(AuthContext); const onLogout = () => { clearToken(); history.push("/"); }; + const [additionalNavs, , isLogoClickable] = useCustomNav(); const classes = useStyles(); + // navbar will be simplified for students + const isOnStudentPages = location.pathname === "/join"; + + const logoLinkWrapper = (children: ReactNode) => ( + {children} + ); + const logoElement = ( + + Fableous + + ); + return ( - - - Fableous - - + {isLogoClickable ? logoLinkWrapper(logoElement) : logoElement}
{/* spacer */} - {isAuthenticated ? ( + {additionalNavs.map(({ icon, label, buttonProps }) => ( + + ))} + {isAuthenticated && !isOnStudentPages && ( <> -
diff --git a/fableous-fe/src/components/canvas/EraserIcon.tsx b/fableous-fe/src/components/canvas/EraserIcon.tsx index 383e5dcc..6b1730f2 100644 --- a/fableous-fe/src/components/canvas/EraserIcon.tsx +++ b/fableous-fe/src/components/canvas/EraserIcon.tsx @@ -4,19 +4,17 @@ import { SvgIcon, SvgIconProps } from "@material-ui/core"; export default function EraserIcon(props: SvgIconProps) { return ( - - - - + + ); diff --git a/fableous-fe/src/containers/ControllerCanvasPage.tsx b/fableous-fe/src/containers/ControllerCanvasPage.tsx index 2afa7129..2795e2a3 100644 --- a/fableous-fe/src/containers/ControllerCanvasPage.tsx +++ b/fableous-fe/src/containers/ControllerCanvasPage.tsx @@ -1,4 +1,4 @@ -import { useRef, useState, useEffect, useCallback } from "react"; +import { useRef, useState, useEffect, useCallback, useMemo } from "react"; import { Button, Card, @@ -20,6 +20,7 @@ import useAxios from "axios-hooks"; import * as yup from "yup"; import { Formik, FormikHelpers } from "formik"; import { useSnackbar } from "notistack"; +import Joyride, { Step, StoreHelpers } from "react-joyride"; import Canvas from "../components/canvas/Canvas"; import { restAPI, wsAPI } from "../api"; import { APIResponse, ControllerJoin, Session } from "../data"; @@ -39,6 +40,9 @@ import { ASPECT_RATIO, SCALE } from "../components/canvas/constants"; import useContainRatio from "../hooks/useContainRatio"; import ChipRow from "../components/ChipRow"; import { colors } from "../colors"; +import { TutorialTargetId } from "../tutorialTargetIds"; +import useTutorial from "../hooks/useTutorial"; +import { useCustomNav } from "../components/CustomNavProvider"; import { proto as pb } from "../proto/message_pb"; enum ControllerState { @@ -56,6 +60,8 @@ const useStyles = makeStyles({ }, }); +const CONTROLLER_TUTORIAL_KEY = "controllerTutorial"; + export default function ControllerCanvasPage() { const { enqueueSnackbar } = useSnackbar(); const [controllerState, setControllerState] = useState( @@ -75,6 +81,21 @@ export default function ControllerCanvasPage() { const [toolColor, setToolColor] = useState("#000000ff"); const [toolMode, setToolMode] = useState(ToolMode.None); const [toolWidth, setToolWidth] = useState(8 * SCALE); + const [tutorialHelper, setTutorialHelper] = useState(); + const [isTutorialRunning, handleJoyrideCallback] = useTutorial({ + showTutorialButton: useMemo( + () => controllerState === ControllerState.DrawingSession, + [controllerState] + ), + localStorageKey: CONTROLLER_TUTORIAL_KEY, + onManualStartCallback: useCallback(() => { + if (tutorialHelper) { + // skip first step + tutorialHelper.next(); + } + }, [tutorialHelper]), + }); + const [, , , setIsNavbarLogoClickable] = useCustomNav(); const canvasContainerRef = useRef( document.createElement("div") ); @@ -215,6 +236,113 @@ export default function ControllerCanvasPage() { setIsDone(true); }; + const commonTutorialSteps: Step[] = useMemo( + () => [ + { + target: `#${TutorialTargetId.NavbarTutorial}`, + content: + "Do you want to go through the tutorial? You can access it anytime by clicking the help icon.", + placement: "bottom", + disableBeacon: true, + // wierdly, close behavior is like next step, unsure on how to fix it + hideCloseButton: true, + }, + { + target: `#${TutorialTargetId.ControllerTopChipRow}`, + content: + "You will be assigned a role and collaboratively draw a story based on a theme.", + placement: "bottom", + disableBeacon: true, + hideCloseButton: true, + }, + { + target: `#${TutorialTargetId.ControllerCanvas}`, + content: + "You will only see your own drawing here, see teacher's hub screen for the combined drawing.", + placement: "center", + disableBeacon: true, + hideCloseButton: true, + }, + ], + [] + ); + + const drawingTutorialSteps: Step[] = useMemo( + () => [ + { + target: `#${TutorialTargetId.BrushTool}`, + content: "Use brush to draw", + placement: "right", + disableBeacon: true, + hideCloseButton: true, + }, + { + target: `#${TutorialTargetId.EraseTool}`, + content: "Use eraser to erase", + placement: "right", + disableBeacon: true, + hideCloseButton: true, + }, + { + target: `#${TutorialTargetId.FillTool}`, + content: "Use bucket to fill with selected colour", + placement: "right", + disableBeacon: true, + hideCloseButton: true, + }, + { + target: `#${TutorialTargetId.PaletteTool}`, + content: "Use palette to choose a colour", + placement: "right", + disableBeacon: true, + hideCloseButton: true, + }, + { + target: `#${TutorialTargetId.UndoTool}`, + content: "Use undo to undo a recent action", + placement: "right", + disableBeacon: true, + hideCloseButton: true, + }, + ], + [] + ); + + const storyTutorialSteps: Step[] = useMemo( + () => [ + { + target: `#${TutorialTargetId.TextTool}`, + content: "Use text to write a story using keyboard", + placement: "right", + disableBeacon: true, + hideCloseButton: true, + }, + { + target: `#${TutorialTargetId.AudioTool}`, + content: "Use microphone to record a story", + placement: "right", + disableBeacon: true, + hideCloseButton: true, + }, + { + target: `#${TutorialTargetId.UndoTool}`, + content: "Use undo to undo a recent action", + placement: "right", + disableBeacon: true, + hideCloseButton: true, + }, + ], + [] + ); + + const tutorialSteps = useMemo( + () => + role === pb.ControllerRole.STORY + ? commonTutorialSteps.concat(storyTutorialSteps) + : commonTutorialSteps.concat(drawingTutorialSteps), + [role, commonTutorialSteps, storyTutorialSteps, drawingTutorialSteps] + ); + // setup event listeners on ws connection useEffect(() => { if (!wsConn) { @@ -276,11 +404,43 @@ export default function ControllerCanvasPage() { ); }, [controllerState, isDone, role, wsConn]); + // prevent student accidentally going to homepage when drawing + useEffect(() => { + if (controllerState === ControllerState.DrawingSession) { + setIsNavbarLogoClickable(false); + + return () => setIsNavbarLogoClickable(true); + } + + return () => {}; + }, [controllerState, setIsNavbarLogoClickable]); + return ( +
,
@@ -577,6 +740,7 @@ export default function ControllerCanvasPage() { > { if (currentPageIdx && story && currentPageIdx > story.pages) { - // TODO send canvas result to backend here - // assume backend will close ws conn enqueueSnackbar("Story completed!", { variant: "success" }); setHubState(HubState.SessionForm); achievementReset(); diff --git a/fableous-fe/src/hooks/useTutorial.ts b/fableous-fe/src/hooks/useTutorial.ts new file mode 100644 index 00000000..5cff0ab8 --- /dev/null +++ b/fableous-fe/src/hooks/useTutorial.ts @@ -0,0 +1,113 @@ +import { useCallback, useEffect, useMemo, useState } from "react"; +import { CallBackProps, STATUS } from "react-joyride"; +import { useCustomNav } from "../components/CustomNavProvider"; +import { getLocalStorage, ONE_DAY, setLocalStorage } from "../storage"; +import { TutorialTargetId } from "../tutorialTargetIds"; + +enum TutorialState { + Off = 0, + AutomaticallyStarted, + ManuallyStarted, +} + +/** + * Auto start tutorial upon showTutorialButton changing to true, + * records usage timestamp and do not auto start it again for specified duration. + * Navbar will have tutorial button whenever showTutorialButton is true. + * + * @param {boolean} showTutorialButton true when tutorial is accessible + * @param {string} localStorageKey un-namespaced localstorage key to store usage timestamp + * @param {() => {}} onManualStartCallback custom logic to run on navbar tutorial button click + * @param {number} duration duration in milliseconds to not automatically start tutorial from most recent use + * + * @return {[boolean, (data: CallBackProps) => void]} tutorial running state and callback function to pass to Joyride component + */ +export default function useTutorial(config: { + showTutorialButton: boolean; + localStorageKey: string; + onManualStartCallback?: () => void; + duration?: number; +}): [boolean, (data: CallBackProps) => void] { + const { + showTutorialButton, + localStorageKey, + onManualStartCallback = () => {}, + duration = ONE_DAY, + } = config; + + const [tutorialState, setTutorialState] = useState( + TutorialState.Off + ); + const [, setAdditionalNavs] = useCustomNav(); + + // state to expose is only boolean + const isRunning = useMemo(() => { + return tutorialState !== TutorialState.Off; + }, [tutorialState]); + + // auto start tutorial when showTutorialButton becomes true + useEffect(() => { + if (showTutorialButton && getLocalStorage(localStorageKey) === null) { + setTutorialState(TutorialState.AutomaticallyStarted); + } + + if (!showTutorialButton) { + setTutorialState(TutorialState.Off); + } + }, [showTutorialButton, localStorageKey]); + + // show tutorial button in navbar anytime showTutorialButton is true + useEffect(() => { + if (showTutorialButton) { + setAdditionalNavs([ + { + icon: "help", + label: "Tutorial", + buttonProps: { + id: TutorialTargetId.NavbarTutorial, + onClick: () => { + setTutorialState(TutorialState.ManuallyStarted); + }, + disabled: isRunning, + }, + }, + ]); + + return () => { + setAdditionalNavs([]); + }; + } + + return () => {}; + }, [showTutorialButton, isRunning, setAdditionalNavs, onManualStartCallback]); + + // remember tutorial use + useEffect(() => { + if (isRunning) { + // set value to be empty string to differentiate from null + setLocalStorage(localStorageKey, "", duration); + } + }, [isRunning, localStorageKey, duration]); + + // exec passed custom logic on manually starting tutorial + useEffect(() => { + if (tutorialState === TutorialState.ManuallyStarted) { + onManualStartCallback(); + } + }, [tutorialState, onManualStartCallback]); + + // handler to be passed to Joyride.callback to close it on skip or finish + const handleJoyrideCallback = useCallback( + (data: CallBackProps) => { + const { status } = data; + const finishedStatuses: string[] = [STATUS.FINISHED, STATUS.SKIPPED]; + + if (finishedStatuses.includes(status)) { + setTutorialState(TutorialState.Off); + } + }, + [setTutorialState] + ); + + return [isRunning, handleJoyrideCallback]; +} diff --git a/fableous-fe/src/storage.ts b/fableous-fe/src/storage.ts new file mode 100644 index 00000000..160cb9d0 --- /dev/null +++ b/fableous-fe/src/storage.ts @@ -0,0 +1,30 @@ +const keyNamespace = "fableous"; + +const getKey = (key: string) => `${keyNamespace}.${key}`; + +export const ONE_DAY = 86400000; + +export const setLocalStorage = (key: string, value: any, ttlInMs?: number) => { + const namespacedKey = getKey(key); + const now = new Date(); + const item = { + value, + ...(ttlInMs !== undefined && { expiry: now.getTime() + ttlInMs }), + }; + localStorage.setItem(namespacedKey, JSON.stringify(item)); +}; + +export const getLocalStorage = (key: string) => { + const namespacedKey = getKey(key); + const itemStr = localStorage.getItem(namespacedKey); + if (!itemStr) { + return null; + } + const item = JSON.parse(itemStr); + const now = new Date(); + if (item.expiry !== undefined && now.getTime() > item.expiry) { + localStorage.removeItem(namespacedKey); + return null; + } + return item.value; +}; diff --git a/fableous-fe/src/tutorialTargetIds.ts b/fableous-fe/src/tutorialTargetIds.ts new file mode 100644 index 00000000..d29062ba --- /dev/null +++ b/fableous-fe/src/tutorialTargetIds.ts @@ -0,0 +1,13 @@ +/* eslint-disable import/prefer-default-export */ +export enum TutorialTargetId { + ControllerCanvas = "controller-canvas", + ControllerTopChipRow = "controller-topChipRow", + NavbarTutorial = "navbar-tutorialButton", + BrushTool = "canvasToolbarIcon-brush", + EraseTool = "canvasToolbarIcon-erase", + FillTool = "canvasToolbarIcon-fill", + PaletteTool = "canvasToolbarIcon-palette", + TextTool = "canvasToolbarIcon-text", + AudioTool = "canvasToolbarIcon-audio", + UndoTool = "canvasToolbarIcon-undo", +} diff --git a/fableous-fe/yarn.lock b/fableous-fe/yarn.lock index 6373e10c..dbb62081 100644 --- a/fableous-fe/yarn.lock +++ b/fableous-fe/yarn.lock @@ -4398,6 +4398,11 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= +deep-diff@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-1.0.2.tgz#afd3d1f749115be965e89c63edc7abb1506b9c26" + integrity sha512-aWS3UIVH+NPGCD1kki+DCU9Dua032iSsO43LqQpcs4R3+dVv7tX0qBGjiVHJHjplsoUM2XRO/KB92glqc68awg== + deep-equal@^1.0.1: version "1.1.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.1.1.tgz#b5c98c942ceffaf7cb051e24e1434a25a2e6076a" @@ -5357,6 +5362,11 @@ execa@^4.0.0: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +exenv@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50= + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -6722,6 +6732,11 @@ is-in-browser@^1.0.2, is-in-browser@^1.1.3: resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835" integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU= +is-lite@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/is-lite/-/is-lite-0.8.1.tgz#a9bd03c90ea723d450c78c991b84f78e7e3126f9" + integrity sha512-ekSwuewzOmwFnzzAOWuA5fRFPqOeTrLIL3GWT7hdVVi+oLuD+Rau8gCmkb94vH5hjXc1Q/CfIW/y/td1RrNQIg== + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -8270,6 +8285,16 @@ neo-async@^2.5.0, neo-async@^2.6.1, neo-async@^2.6.2: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== +nested-property@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-1.0.1.tgz#2001105b5c69413411b876bba9b86f4316af613f" + integrity sha512-BnBBoo/8bBNRdAnJc7+m79oWk7dXwW1+vCesaEQhfDGVwXGLMvmI4NwYgLTW94R/x+R2s/yr2g/hB/4w/YSAvA== + +nested-property@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/nested-property/-/nested-property-4.0.0.tgz#a67b5a31991e701e03cdbaa6453bc5b1011bb88d" + integrity sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA== + next-tick@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c" @@ -8965,6 +8990,11 @@ popper.js@1.16.1-lts: resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1-lts.tgz#cf6847b807da3799d80ee3d6d2f90df8a3f50b05" integrity sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA== +popper.js@^1.16.0: + version "1.16.1" + resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" + integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== + portfinder@^1.0.26: version "1.0.28" resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.28.tgz#67c4622852bd5374dd1dd900f779f53462fac778" @@ -10082,7 +10112,19 @@ react-fast-compare@^2.0.1: resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== -react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: +react-floater@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/react-floater/-/react-floater-0.7.3.tgz#f57947960682586866ec21540e73c9049ca9f787" + integrity sha512-d1wAEph+xRxQ0RJ3woMmYLlZHTaCIsja7Bv6JNo2ezsVUgdMan4CxOR4Do4/xgpmRFfsQMdlygexLAZZypWirw== + dependencies: + deepmerge "^4.2.2" + exenv "^1.2.2" + is-lite "^0.8.1" + popper.js "^1.16.0" + react-proptype-conditional-require "^1.0.4" + tree-changes "^0.5.1" + +react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== @@ -10097,6 +10139,27 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== +react-joyride@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/react-joyride/-/react-joyride-2.3.1.tgz#de3c8e8bfd6b58f62e7c6e8b38467e807ef8f90f" + integrity sha512-MmyhECU3V+4kZAJrcDPPXcXxaoTpwc7g+E7Cq6QZ5IqJZrWYSVvpVCfudQcdcf6BsNbgawRhvCvbQyeWoPtNig== + dependencies: + deep-diff "^1.0.2" + deepmerge "^4.2.2" + exenv "^1.2.2" + is-lite "^0.8.1" + nested-property "^4.0.0" + react-floater "^0.7.3" + react-is "^16.13.1" + scroll "^3.0.1" + scrollparent "^2.0.1" + tree-changes "^0.7.1" + +react-proptype-conditional-require@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/react-proptype-conditional-require/-/react-proptype-conditional-require-1.0.4.tgz#69c2d5741e6df5e08f230f36bbc2944ee1222555" + integrity sha1-acLVdB5t9eCPIw82u8KUTuEiJVU= + react-refresh@^0.8.3: version "0.8.3" resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f" @@ -10791,6 +10854,16 @@ schema-utils@^3.0.0: ajv "^6.12.5" ajv-keywords "^3.5.2" +scroll@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/scroll/-/scroll-3.0.1.tgz#d5afb59fb3592ee3df31c89743e78b39e4cd8a26" + integrity sha512-pz7y517OVls1maEzlirKO5nPYle9AXsFzTMNJrRGmT951mzpIBy7sNHOg5o/0MQd/NqliCiWnAi0kZneMPFLcg== + +scrollparent@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/scrollparent/-/scrollparent-2.0.1.tgz#715d5b9cc57760fb22bdccc3befb5bfe06b1a317" + integrity sha1-cV1bnMV3YPsivczDvvtb/gaxoxc= + select-hose@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/select-hose/-/select-hose-2.0.0.tgz#625d8658f865af43ec962bfc376a37359a4994ca" @@ -11797,6 +11870,22 @@ tr46@^2.0.2: dependencies: punycode "^2.1.1" +tree-changes@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/tree-changes/-/tree-changes-0.5.1.tgz#e31cc8a0f56c8c401f0a88243d9165dbea4f570c" + integrity sha512-O873xzV2xRZ6N059Mn06QzmGKEE21LlvIPbsk2G+GS9ZX5OCur6PIwuuh0rWpAPvLWQZPj0XObyG27zZyLHUzw== + dependencies: + deep-diff "^1.0.2" + nested-property "1.0.1" + +tree-changes@^0.7.1: + version "0.7.1" + resolved "https://registry.yarnpkg.com/tree-changes/-/tree-changes-0.7.1.tgz#fa8810cbe417e80b9a42c4b018f934c7ad8fa156" + integrity sha512-sPIt8PKDi0OQTglr7lsetcB9DU19Ls/ZgFSjFvK6DWJGisAn4sOxtjpmQfuqjexQE4UU9U53LNmataL1kRJ3Uw== + dependencies: + fast-deep-equal "^3.1.3" + is-lite "^0.8.1" + tryer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/tryer/-/tryer-1.0.1.tgz#f2c85406800b9b0f74c9f7465b81eaad241252f8"