diff --git a/frontend/package.json b/frontend/package.json index 20486293a3..8245fbb88d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -90,13 +90,14 @@ "prop-types": "15.8.1", "react": "17.0.2", "react-chartjs-2": "4.3.1", + "react-confetti": "^6.1.0", "react-dom": "17.0.2", "react-dropzone": "14.2.3", "react-error-boundary": "3.1.4", "react-hooks-global-state": "2.1.0", "react-joyride": "^2.5.3", - "react-markdown": "^8.0.4", "react-linkify": "^1.0.0-alpha", + "react-markdown": "^8.0.4", "react-router-dom": "6.8.1", "react-table": "7.8.0", "react-test-renderer": "17.0.2", diff --git a/frontend/src/component/demo/Demo.tsx b/frontend/src/component/demo/Demo.tsx index 33f779f473..9fcf68a4ca 100644 --- a/frontend/src/component/demo/Demo.tsx +++ b/frontend/src/component/demo/Demo.tsx @@ -1,17 +1,15 @@ -import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import { useEffect, useState } from 'react'; import { DemoTopics } from './DemoTopics/DemoTopics'; import { DemoSteps } from './DemoSteps/DemoSteps'; -import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { createLocalStorage } from 'utils/createLocalStorage'; import { TOPICS } from './demo-topics'; import { DemoDialogWelcome } from './DemoDialog/DemoDialogWelcome/DemoDialogWelcome'; +import { DemoDialogFinish } from './DemoDialog/DemoDialogFinish/DemoDialogFinish'; const defaultProgress = { welcomeOpen: true, expanded: true, - active: false, - topic: 0, + topic: -1, steps: [0], }; @@ -19,34 +17,47 @@ const { value: storedProgress, setValue: setStoredProgress } = createLocalStorage('Tutorial:v1', defaultProgress); export const Demo = () => { - const { uiConfig } = useUiConfig(); const [welcomeOpen, setWelcomeOpen] = useState( - storedProgress.welcomeOpen ?? true + storedProgress.welcomeOpen ?? defaultProgress.welcomeOpen ); - const [active, setActive] = useState(false); - const [expanded, setExpanded] = useState(storedProgress.expanded ?? true); - const [topic, setTopic] = useState(storedProgress.topic ?? 0); - const [steps, setSteps] = useState(storedProgress.steps ?? [0]); + const [finishOpen, setFinishOpen] = useState(false); - useEffect(() => { - if (storedProgress.active) { - setTimeout(() => { - setActive(true); - }, 1000); - } - }, []); + const [expanded, setExpanded] = useState( + storedProgress.expanded ?? defaultProgress.expanded + ); + const [topic, setTopic] = useState( + storedProgress.topic ?? defaultProgress.topic + ); + const [steps, setSteps] = useState( + storedProgress.steps ?? defaultProgress.steps + ); useEffect(() => { setStoredProgress({ welcomeOpen, expanded, - active, topic, steps, }); - }, [welcomeOpen, expanded, active, topic, steps]); + }, [welcomeOpen, expanded, topic, steps]); - if (!uiConfig.flags.demo) return null; + const onStart = () => { + setTopic(0); + setSteps([0]); + setExpanded(true); + }; + + const onFinish = () => { + const completedSteps = steps.reduce( + (acc, step) => acc + (step || 0), + 1 + ); + const totalSteps = TOPICS.flatMap(({ steps }) => steps).length; + + if (completedSteps === totalSteps) { + setFinishOpen(true); + } + }; return ( <> @@ -58,7 +69,17 @@ export const Demo = () => { }} onStart={() => { setWelcomeOpen(false); - setActive(true); + onStart(); + }} + /> + { + setFinishOpen(false); + }} + onRestart={() => { + setFinishOpen(false); + onStart(); }} /> { }); }} topics={TOPICS} - onShowWelcome={() => setWelcomeOpen(true)} + onWelcome={() => setWelcomeOpen(true)} /> - - } + ); diff --git a/frontend/src/component/demo/DemoDialog/DemoDialogFinish/DemoDialogFinish.tsx b/frontend/src/component/demo/DemoDialog/DemoDialogFinish/DemoDialogFinish.tsx new file mode 100644 index 0000000000..98e17f1af7 --- /dev/null +++ b/frontend/src/component/demo/DemoDialog/DemoDialogFinish/DemoDialogFinish.tsx @@ -0,0 +1,66 @@ +import { Button, Typography, styled } from '@mui/material'; +import { DemoDialog } from '../DemoDialog'; +import Confetti from 'react-confetti'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; + +const StyledActions = styled('div')(({ theme }) => ({ + display: 'grid', + gridTemplateColumns: '1fr 1fr', + gap: theme.spacing(3), + marginTop: theme.spacing(7.5), +})); + +const StyledButton = styled(Button)(({ theme }) => ({ + height: theme.spacing(7), +})); + +interface IDemoDialogFinishProps { + open: boolean; + onClose: () => void; + onRestart: () => void; +} + +export const DemoDialogFinish = ({ + open, + onClose, + onRestart, +}: IDemoDialogFinishProps) => ( + <> + + } + /> + + You finished the tutorial + + Great job! Keep exploring Unleash, as this was just a small + example of its full potential. You can do the tutorial again at + any moment. + + + + Restart tutorial + + + Close + + + + +); diff --git a/frontend/src/component/demo/DemoSteps/DemoSteps.tsx b/frontend/src/component/demo/DemoSteps/DemoSteps.tsx index 88158d3b1e..9fd49e1381 100644 --- a/frontend/src/component/demo/DemoSteps/DemoSteps.tsx +++ b/frontend/src/component/demo/DemoSteps/DemoSteps.tsx @@ -50,6 +50,7 @@ interface IDemoStepsProps { topic: number; setTopic: React.Dispatch>; topics: ITutorialTopic[]; + onFinish: () => void; } export const DemoSteps = ({ @@ -59,54 +60,58 @@ export const DemoSteps = ({ topic, setTopic, topics, + onFinish, }: IDemoStepsProps) => { const theme = useTheme(); const navigate = useNavigate(); const location = useLocation(); const [run, setRun] = useState(false); - const [flow, setFlow] = useState<'next' | 'back'>('next'); + const [flow, setFlow] = useState<'next' | 'back' | 'load'>('load'); const abortController = new AbortController(); - const skip = () => { - abortController.abort(); - setTopic(-1); - setExpanded(false); + const setTopicStep = (topic: number, step?: number) => { + setRun(false); + setTopic(topic); + if (step !== undefined) { + setSteps(steps => { + const newSteps = [...steps]; + newSteps[topic] = step; + return newSteps; + }); + } }; - const setStep = (topic: number, step: number) => { - setSteps(steps => { - const newSteps = [...steps]; - newSteps[topic] = step; - return newSteps; - }); + const skip = () => { + abortController.abort(); + setTopicStep(-1); + setExpanded(false); }; const back = () => { setFlow('back'); if (steps[topic] === 0) { const newTopic = topic - 1; - setTopic(newTopic); - setStep(newTopic, topics[newTopic].steps.length - 1); + setTopicStep(newTopic, topics[newTopic].steps.length - 1); } else { - setStep(topic, steps[topic] - 1); + setTopicStep(topic, steps[topic] - 1); } }; const nextTopic = () => { if (topic === topics.length - 1) { - setTopic(-1); + setTopicStep(-1); setExpanded(false); + onFinish(); } else { const newTopic = topic + 1; - setTopic(newTopic); - setStep(newTopic, 0); + setTopicStep(newTopic, 0); } }; const next = (index = steps[topic]) => { setFlow('next'); - setStep(topic, index + 1); + setTopicStep(topic, index + 1); if (index === topics[topic].steps.length - 1) { nextTopic(); } @@ -146,14 +151,6 @@ export const DemoSteps = ({ } } } - - if (run && !document.querySelector(step.target as string)) { - if (step.optional && flow === 'next') { - next(); - } else { - back(); - } - } }; const onBack = (step: ITutorialTopicStep) => { @@ -172,22 +169,39 @@ export const DemoSteps = ({ back(); }; + const waitForLoad = (step: ITutorialTopicStep, tries = 0) => { + setTimeout(() => { + if (document.querySelector(step.target as string)) { + setRun(true); + } else { + if (flow === 'next' && step.optional) { + next(); + } else if (flow === 'back' || tries > 4) { + back(); + } else { + waitForLoad(step, tries + 1); + } + } + }, 200); + }; + useEffect(() => { - setRun(false); if (topic === -1) return; const currentTopic = topics[topic]; - const currentStep = steps[topic]; - const href = currentTopic.steps[currentStep]?.href; - if (href && location.pathname !== href) { - navigate(href); - } - currentTopic.setup?.(); + const currentStepIndex = steps[topic]; + const currentStep = currentTopic.steps[currentStepIndex]; + if (!currentStep) return; - setTimeout(() => { - setRun(true); - }, 200); + if (currentStep.href && location.pathname !== currentStep.href) { + navigate(currentStep.href); + } + waitForLoad(currentStep); }, [topic, steps]); + useEffect(() => { + if (topic > -1) topics[topic].setup?.(); + }, [topic]); + if (topic === -1) return null; const joyrideSteps = topics[topic].steps.map(step => ({ diff --git a/frontend/src/component/demo/DemoTopics/DemoTopics.tsx b/frontend/src/component/demo/DemoTopics/DemoTopics.tsx index b9a33ffd46..e96bb22ceb 100644 --- a/frontend/src/component/demo/DemoTopics/DemoTopics.tsx +++ b/frontend/src/component/demo/DemoTopics/DemoTopics.tsx @@ -138,7 +138,7 @@ interface IDemoTopicsProps { currentTopic: number; setCurrentTopic: (topic: number) => void; topics: ITutorialTopic[]; - onShowWelcome: () => void; + onWelcome: () => void; } export const DemoTopics = ({ @@ -148,7 +148,7 @@ export const DemoTopics = ({ currentTopic, setCurrentTopic, topics, - onShowWelcome, + onWelcome, }: IDemoTopicsProps) => { const completedSteps = steps.reduce((acc, step) => acc + (step || 0), 0); const totalSteps = topics.flatMap(({ steps }) => steps).length; @@ -203,7 +203,7 @@ export const DemoTopics = ({ ); })} - + View demo link again diff --git a/frontend/src/component/demo/demo-topics.tsx b/frontend/src/component/demo/demo-topics.tsx index 974254f1d8..328e080b83 100644 --- a/frontend/src/component/demo/demo-topics.tsx +++ b/frontend/src/component/demo/demo-topics.tsx @@ -185,6 +185,7 @@ export const TOPICS: ITutorialTopic[] = [ ), optional: true, + backCloseModal: true, }, ], }, @@ -289,6 +290,7 @@ export const TOPICS: ITutorialTopic[] = [ ), optional: true, + backCloseModal: true, }, ], }, diff --git a/frontend/yarn.lock b/frontend/yarn.lock index ac7f695cc4..aa50df0257 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -7498,6 +7498,13 @@ react-chartjs-2@4.3.1: resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-4.3.1.tgz#9941e7397fb963f28bb557addb401e9ff96c6681" integrity sha512-5i3mjP6tU7QSn0jvb8I4hudTzHJqS8l00ORJnVwI2sYu0ihpj83Lv2YzfxunfxTZkscKvZu2F2w9LkwNBhj6xA== +react-confetti@^6.1.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/react-confetti/-/react-confetti-6.1.0.tgz#03dc4340d955acd10b174dbf301f374a06e29ce6" + integrity sha512-7Ypx4vz0+g8ECVxr88W9zhcQpbeujJAVqL14ZnXJ3I23mOI9/oBVTQ3dkJhUmB0D6XOtCZEM6N0Gm9PMngkORw== + dependencies: + tween-functions "^1.2.0" + react-dom@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23" @@ -8616,6 +8623,11 @@ tunnel-agent@^0.6.0: dependencies: safe-buffer "^5.0.1" +tween-functions@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tween-functions/-/tween-functions-1.2.0.tgz#1ae3a50e7c60bb3def774eac707acbca73bbc3ff" + integrity sha512-PZBtLYcCLtEcjL14Fzb1gSxPBeL7nWvGhO5ZFPGqziCcr8uvHp0NDmdjBchp6KHL+tExcg0m3NISmKxhU394dA== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"