From d8753670d55f0e5d1a1d79696cf9e0e68333ffff Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Sun, 10 Mar 2024 22:12:39 +0600 Subject: [PATCH 1/5] Intro tour: added completable intro tour --- .../components/community-selector/index.tsx | 1 + src/common/components/tag-selector/index.tsx | 5 +- src/common/features/ui/core/index.tsx | 10 +- .../features/ui/core/intro-step.interface.ts | 5 + src/common/features/ui/index.ts | 1 + src/common/features/ui/intro-tour/index.tsx | 121 ++++++++++++++++++ src/common/pages/submit/index.tsx | 31 +++++ 7 files changed, 172 insertions(+), 2 deletions(-) create mode 100644 src/common/features/ui/core/intro-step.interface.ts create mode 100644 src/common/features/ui/intro-tour/index.tsx diff --git a/src/common/components/community-selector/index.tsx b/src/common/components/community-selector/index.tsx index 80f2a284ca5..fad1dd5c3d2 100644 --- a/src/common/components/community-selector/index.tsx +++ b/src/common/components/community-selector/index.tsx @@ -256,6 +256,7 @@ export class CommunitySelector extends BaseComponent { <> { e.preventDefault(); diff --git a/src/common/components/tag-selector/index.tsx b/src/common/components/tag-selector/index.tsx index 71524a1a0d6..e28caedeb27 100644 --- a/src/common/components/tag-selector/index.tsx +++ b/src/common/components/tag-selector/index.tsx @@ -195,7 +195,10 @@ export class TagSelector extends Component { return ( <> -
0 ? "has-tags" : ""}`)}> +
0 ? "has-tags" : ""}`)} + > { return ( diff --git a/src/common/features/ui/core/index.tsx b/src/common/features/ui/core/index.tsx index 2410170d150..cdad1f91552 100644 --- a/src/common/features/ui/core/index.tsx +++ b/src/common/features/ui/core/index.tsx @@ -1,6 +1,8 @@ import React, { createContext, PropsWithChildren } from "react"; import { useSet } from "react-use"; +export * from "./intro-step.interface"; + export const UIContext = createContext<{ openPopovers: Set; addOpenPopover: (v: string) => void; @@ -17,7 +19,13 @@ export function UIManager({ children }: PropsWithChildren) { ); return ( - + {children} ); diff --git a/src/common/features/ui/core/intro-step.interface.ts b/src/common/features/ui/core/intro-step.interface.ts new file mode 100644 index 00000000000..e2fc0c87371 --- /dev/null +++ b/src/common/features/ui/core/intro-step.interface.ts @@ -0,0 +1,5 @@ +export interface IntroStep { + title: string; + message: string; + targetSelector: string; +} diff --git a/src/common/features/ui/index.ts b/src/common/features/ui/index.ts index b4971613af3..a978236bfe7 100644 --- a/src/common/features/ui/index.ts +++ b/src/common/features/ui/index.ts @@ -10,3 +10,4 @@ export * from "./alert"; export * from "./badge"; export * from "./pagination"; export * from "./core"; +export * from "./intro-tour"; diff --git a/src/common/features/ui/intro-tour/index.tsx b/src/common/features/ui/intro-tour/index.tsx new file mode 100644 index 00000000000..83f0580fcae --- /dev/null +++ b/src/common/features/ui/intro-tour/index.tsx @@ -0,0 +1,121 @@ +import React, { useEffect, useMemo, useState } from "react"; +import { IntroStep } from "@ui/core"; +import { useMountedState } from "react-use"; +import { usePopper } from "react-popper"; +import { createPortal } from "react-dom"; +import { classNameObject } from "../../../helper/class-name-object"; +import { Button } from "@ui/button"; +import useLocalStorage from "react-use/lib/useLocalStorage"; +import { PREFIX } from "../../../util/local-storage"; + +interface Props { + steps: IntroStep[]; + id: string; + enabled: boolean; +} + +export function IntroTour({ steps, id, enabled }: Props) { + const [currentStep, setCurrentStep, clearCurrentStep] = useLocalStorage( + PREFIX + `_it_${id}`, + undefined + ); + const [isFinished, setIsFinished] = useLocalStorage(PREFIX + `_itf_${id}`, false); + + const [host, setHost] = useState(); + const [popperElement, setPopperElement] = useState(); + + const isMounted = useMountedState(); + const popper = usePopper(host, popperElement, { + placement: "top" + }); + + const step = useMemo( + () => (typeof currentStep === "number" ? steps[currentStep] : undefined), + [currentStep, steps] + ); + const totalSteps = useMemo(() => steps.length, [steps]); + const isFirstStep = useMemo( + () => typeof currentStep === "number" && currentStep > 0, + [currentStep] + ); + const isLastStep = useMemo(() => steps.length - 1 === currentStep, [steps, currentStep]); + + // Detect enablement and set default step if there aren't any persistent step + useEffect(() => { + if (typeof currentStep === "undefined" && !isFinished && enabled) { + setCurrentStep(0); + } + }, [currentStep, enabled, isFinished]); + + // Re-attach host element based on host element + useEffect(() => { + if (step) { + const nextHost = document.querySelector(step.targetSelector); + setHost(nextHost); + + if (nextHost) { + nextHost.classList.add("z-[1041]"); + nextHost.classList.add("relative"); + (nextHost as HTMLElement).focus(); + } + } else { + host?.classList.remove("z-[1041]"); + host?.classList.remove("relative"); + setHost(null); + } + }, [step]); + + const nextStep = () => { + if (typeof currentStep === "number" && currentStep < totalSteps - 1) { + setCurrentStep(currentStep + 1); + } + }; + + const prevStep = () => { + if (typeof currentStep === "number" && currentStep > 0) { + setCurrentStep(currentStep - 1); + } + }; + + const finish = () => { + clearCurrentStep(); + setIsFinished(true); + }; + + return isMounted() && !isFinished ? ( + <> + {createPortal( +
, + document.querySelector("#modal-overlay-container")!! + )} + {step && + createPortal( +
+
{step?.title}
+
{step?.message}
+
+ {isFirstStep && ( + + )} + {!isLastStep && } + {isLastStep && } +
+
, + document.querySelector("#popper-container")!! + )} + + ) : ( + <> + ); +} diff --git a/src/common/pages/submit/index.tsx b/src/common/pages/submit/index.tsx index e6331a2d73c..375beeca871 100644 --- a/src/common/pages/submit/index.tsx +++ b/src/common/pages/submit/index.tsx @@ -60,6 +60,8 @@ import { Button } from "@ui/button"; import { dotsMenuIconSvg } from "../../components/decks/icons"; import { Spinner } from "@ui/spinner"; import { FormControl } from "@ui/input"; +import { IntroTour } from "@ui/intro-tour"; +import { IntroStep } from "@ui/core"; interface MatchProps { match: MatchType; @@ -95,6 +97,31 @@ export function Submit(props: PageProps & MatchProps) { // Misc const [editingEntry, setEditingEntry] = useState(null); const [editingDraft, setEditingDraft] = useState(null); + const introSteps = useMemo( + () => [ + { + title: "Title", + message: "My title", + targetSelector: "#submit-title" + }, + { + title: "Tags selector", + message: "My tags selector", + targetSelector: "#submit-tags-selector" + }, + { + title: "Post body", + message: "My post body", + targetSelector: "#the-editor" + }, + { + title: "Community", + message: "My community", + targetSelector: "#community-picker" + } + ], + [] + ); let _updateTimer: any; // todo think about it @@ -381,6 +408,9 @@ export function Submit(props: PageProps & MatchProps) { {clearModal && setClearModal(false)} />} + + +
{editingEntry === null && activeUser && ( @@ -414,6 +444,7 @@ export function Submit(props: PageProps & MatchProps) { />
Date: Sun, 10 Mar 2024 22:44:18 +0600 Subject: [PATCH 2/5] Intro tour: updated submit intro steps and intro styles --- .../components/editor-toolbar/index.tsx | 6 +++- src/common/features/ui/intro-tour/index.scss | 4 +++ src/common/features/ui/intro-tour/index.tsx | 23 +++++++----- src/common/pages/submit/_index.scss | 4 +-- src/common/pages/submit/index.tsx | 35 ++++++++++++++----- 5 files changed, 52 insertions(+), 20 deletions(-) create mode 100644 src/common/features/ui/intro-tour/index.scss diff --git a/src/common/components/editor-toolbar/index.tsx b/src/common/components/editor-toolbar/index.tsx index b8a502aadf0..f5115182860 100644 --- a/src/common/components/editor-toolbar/index.tsx +++ b/src/common/components/editor-toolbar/index.tsx @@ -291,7 +291,11 @@ export function EditorToolbar({ return ( <> -
+
{formatBoldSvg} diff --git a/src/common/features/ui/intro-tour/index.scss b/src/common/features/ui/intro-tour/index.scss new file mode 100644 index 00000000000..abcaaa13712 --- /dev/null +++ b/src/common/features/ui/intro-tour/index.scss @@ -0,0 +1,4 @@ +.intro-tour-focused { + z-index: 1041 !important; + position: relative !important; +} \ No newline at end of file diff --git a/src/common/features/ui/intro-tour/index.tsx b/src/common/features/ui/intro-tour/index.tsx index 83f0580fcae..3673163c6c3 100644 --- a/src/common/features/ui/intro-tour/index.tsx +++ b/src/common/features/ui/intro-tour/index.tsx @@ -7,6 +7,7 @@ import { classNameObject } from "../../../helper/class-name-object"; import { Button } from "@ui/button"; import useLocalStorage from "react-use/lib/useLocalStorage"; import { PREFIX } from "../../../util/local-storage"; +import "./index.scss"; interface Props { steps: IntroStep[]; @@ -49,18 +50,16 @@ export function IntroTour({ steps, id, enabled }: Props) { // Re-attach host element based on host element useEffect(() => { + host?.classList.remove("intro-tour-focused"); + if (step) { const nextHost = document.querySelector(step.targetSelector); setHost(nextHost); if (nextHost) { - nextHost.classList.add("z-[1041]"); - nextHost.classList.add("relative"); - (nextHost as HTMLElement).focus(); + nextHost.classList.add("intro-tour-focused"); } } else { - host?.classList.remove("z-[1041]"); - host?.classList.remove("relative"); setHost(null); } }, [step]); @@ -87,22 +86,28 @@ export function IntroTour({ steps, id, enabled }: Props) { {createPortal(
finish()} />, document.querySelector("#modal-overlay-container")!! )} {step && createPortal(
-
{step?.title}
+
+ + {currentStep! + 1} of {steps.length} + + {step?.title} +
{step?.message}
-
+
{isFirstStep && ( )} - +
+ + + ' +
From 24aa5be449877fb62e7fee0012ddb54f8f161c74 Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Sun, 10 Mar 2024 23:08:08 +0600 Subject: [PATCH 4/5] Intro tour: updated host highlighting by clipping --- src/common/features/ui/intro-tour/index.scss | 2 +- src/common/features/ui/intro-tour/index.tsx | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/common/features/ui/intro-tour/index.scss b/src/common/features/ui/intro-tour/index.scss index abcaaa13712..03717deb8ea 100644 --- a/src/common/features/ui/intro-tour/index.scss +++ b/src/common/features/ui/intro-tour/index.scss @@ -1,4 +1,4 @@ .intro-tour-focused { - z-index: 1041 !important; + //z-index: 1041 !important; position: relative !important; } \ No newline at end of file diff --git a/src/common/features/ui/intro-tour/index.tsx b/src/common/features/ui/intro-tour/index.tsx index 4ff7929e35f..01487b0638c 100644 --- a/src/common/features/ui/intro-tour/index.tsx +++ b/src/common/features/ui/intro-tour/index.tsx @@ -8,6 +8,7 @@ import { Button } from "@ui/button"; import useLocalStorage from "react-use/lib/useLocalStorage"; import { PREFIX } from "../../../util/local-storage"; import "./index.scss"; +import { DOMRect } from "sortablejs"; interface Props { steps: IntroStep[]; @@ -26,6 +27,7 @@ export function IntroTour({ steps, id, enabled, forceActivation, setForceActivat const [host, setHost] = useState(); const [popperElement, setPopperElement] = useState(); + const [hostRect, setHostRect] = useState(); const isMounted = useMountedState(); const popper = usePopper(host, popperElement, { @@ -42,6 +44,19 @@ export function IntroTour({ steps, id, enabled, forceActivation, setForceActivat [currentStep] ); const isLastStep = useMemo(() => steps.length - 1 === currentStep, [steps, currentStep]); + const clipPath = useMemo( + () => + hostRect + ? `polygon(0% 0%, 0% 100%, ${hostRect.x}px 100%, ${hostRect.x}px ${hostRect.y}px, ${ + hostRect.x + hostRect.width + }px ${hostRect.y}px, ${hostRect.x + hostRect.width}px ${ + hostRect.y + hostRect.height + }px, ${hostRect.x}px ${hostRect.y + hostRect.height}px, ${ + hostRect.x + }px 100%, 100% 100%, 100% 0%)` + : "unset", + [hostRect] + ); // Detect enablement and set default step if there aren't any persistent step useEffect(() => { @@ -68,6 +83,7 @@ export function IntroTour({ steps, id, enabled, forceActivation, setForceActivat setHost(nextHost); if (nextHost) { + setHostRect(nextHost.getBoundingClientRect()); nextHost.classList.add("intro-tour-focused"); } } else { @@ -97,8 +113,9 @@ export function IntroTour({ steps, id, enabled, forceActivation, setForceActivat {createPortal(
finish()} />, document.querySelector("#modal-overlay-container")!! From 77140cde0c348df4e7fc10cf0c78e4aa6c65328d Mon Sep 17 00:00:00 2001 From: "ildar.timerbaev" Date: Thu, 14 Mar 2024 23:45:11 +0600 Subject: [PATCH 5/5] Tour: added info button instead of take a tour --- src/common/i18n/locales/en-US.json | 1 + src/common/pages/submit/index.tsx | 52 +++++++++++++++++------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index 6ee0e39d643..9ab30fc35cb 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -1436,6 +1436,7 @@ "title-placeholder": "Title", "body-placeholder": "Tell your story...", "reward": "Reward", + "take-tour": "Take a tour", "reward-hint": "Set author reward ratio for liquid and staked tokens", "reward-default": "Default 50% / 50%", "reward-sp": "Power Up 100%", diff --git a/src/common/pages/submit/index.tsx b/src/common/pages/submit/index.tsx index c441c4234a9..fb0cbc5572b 100644 --- a/src/common/pages/submit/index.tsx +++ b/src/common/pages/submit/index.tsx @@ -35,7 +35,13 @@ import _c from "../../util/fix-class-names"; import TextareaAutocomplete from "../../components/textarea-autocomplete"; import { AvailableCredits } from "../../components/available-credits"; import ClickAwayListener from "../../components/clickaway-listener"; -import { checkSvg, contentLoadSvg, contentSaveSvg, helpIconSvg } from "../../img/svg"; +import { + checkSvg, + contentLoadSvg, + contentSaveSvg, + helpIconSvg, + informationSvg +} from "../../img/svg"; import { BeneficiaryEditorDialog } from "../../components/beneficiary-editor"; import PostScheduler from "../../components/post-scheduler"; import moment from "moment/moment"; @@ -57,12 +63,11 @@ import { SubmitVideoAttachments } from "./submit-video-attachments"; import { useThreeSpeakMigrationAdapter } from "./hooks/three-speak-migration-adapter"; import ModalConfirm from "@ui/modal-confirm"; import { Button } from "@ui/button"; -import { dotsMenuIconSvg } from "../../components/decks/icons"; import { Spinner } from "@ui/spinner"; import { FormControl } from "@ui/input"; import { IntroTour } from "@ui/intro-tour"; import { IntroStep } from "@ui/core"; -import { UilStar } from "@iconscout/react-unicons"; +import { dotsMenuIconSvg } from "../../components/decks/icons"; interface MatchProps { match: MatchType; @@ -99,6 +104,7 @@ export function Submit(props: PageProps & MatchProps) { // Misc const [editingEntry, setEditingEntry] = useState(null); const [editingDraft, setEditingDraft] = useState(null); + const [isTourFinished] = useLocalStorage(PREFIX + `_itf_submit`, false); const tourEnabled = useMemo(() => !activeUser, [activeUser]); const introSteps = useMemo( @@ -439,7 +445,7 @@ export function Submit(props: PageProps & MatchProps) {
{editingEntry === null && activeUser && ( -
+
+ +
+ +
)} )} - -
- - - ' -
+