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/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/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.scss b/src/common/features/ui/intro-tour/index.scss new file mode 100644 index 00000000000..03717deb8ea --- /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 new file mode 100644 index 00000000000..01487b0638c --- /dev/null +++ b/src/common/features/ui/intro-tour/index.tsx @@ -0,0 +1,154 @@ +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"; +import "./index.scss"; +import { DOMRect } from "sortablejs"; + +interface Props { + steps: IntroStep[]; + id: string; + enabled: boolean; + forceActivation: boolean; + setForceActivation: (v: boolean) => void; +} + +export function IntroTour({ steps, id, enabled, forceActivation, setForceActivation }: 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 [hostRect, setHostRect] = 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]); + 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(() => { + if (typeof currentStep === "undefined" && !isFinished && enabled) { + setCurrentStep(0); + } + }, [currentStep, enabled, isFinished]); + + useEffect(() => { + if (forceActivation) { + setCurrentStep(0); + setIsFinished(false); + + setForceActivation(false); + } + }, [forceActivation]); + + // 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) { + setHostRect(nextHost.getBoundingClientRect()); + nextHost.classList.add("intro-tour-focused"); + } + } else { + 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( +
finish()} + />, + document.querySelector("#modal-overlay-container")!! + )} + {step && + createPortal( +
+
+ + {currentStep! + 1} of {steps.length} + + {step?.title} +
+
{step?.message}
+
+ {isFirstStep && ( + + )} + {!isLastStep && } + {isLastStep && } +
+
, + document.querySelector("#popper-container")!! + )} + + ) : ( + <> + ); +} diff --git a/src/common/i18n/locales/en-US.json b/src/common/i18n/locales/en-US.json index a4fc1b82b39..155a9eacbf7 100644 --- a/src/common/i18n/locales/en-US.json +++ b/src/common/i18n/locales/en-US.json @@ -1438,6 +1438,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%", @@ -2387,5 +2388,15 @@ "title": "Boost+", "sub-title": "Boost your account using Ecency points", "already-boosted-account": "This account boosted already" + }, + "submit-tour": { + "title": "Post creating", + "title-hint": "You may to set any title for your post", + "tags-hint": "Tags helps to find out your post by special keywords", + "body-hint": "Post body is rich text which may contain various of different components, styles", + "community-hint": "Post may be attached to specific community which helps to promote your post within community members", + "toolbar-hint": "Toolbar allow to insert a lot of different components, apply formatting, insert images, videos and links", + "advanced-hint": "Configure advanced settings such as scheduling, beneficiaries", + "help-hint": "Have any questions? Check out help center" } } diff --git a/src/common/pages/submit/_index.scss b/src/common/pages/submit/_index.scss index c036ca89c8b..4e7af1f1d19 100644 --- a/src/common/pages/submit/_index.scss +++ b/src/common/pages/submit/_index.scss @@ -72,9 +72,9 @@ background: transparent; border: none; - &:focus { + &:focus, &.intro-tour-focused { @include themify(day) { - background: lighten($yellow, 46); + @apply bg-warning-046; } @include themify(night) { diff --git a/src/common/pages/submit/index.tsx b/src/common/pages/submit/index.tsx index e6331a2d73c..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,9 +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 { dotsMenuIconSvg } from "../../components/decks/icons"; interface MatchProps { match: MatchType; @@ -91,10 +99,54 @@ export function Submit(props: PageProps & MatchProps) { const [drafts, setDrafts] = useState(false); const [showHelp, setShowHelp] = useState(false); const [isDraftEmpty, setIsDraftEmpty] = useState(false); + const [forceReactivateTour, setForceReactivateTour] = useState(false); // 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( + () => [ + { + title: _t("submit-tour.title"), + message: _t("submit-tour.title-hint"), + targetSelector: "#submit-title" + }, + { + title: _t("submit-tour.title"), + message: _t("submit-tour.tags-hint"), + targetSelector: "#submit-tags-selector" + }, + { + title: _t("submit-tour.title"), + message: _t("submit-tour.body-hint"), + targetSelector: "#the-editor" + }, + { + title: _t("submit-tour.title"), + message: _t("submit-tour.community-hint"), + targetSelector: "#community-picker" + }, + { + title: _t("submit-tour.title"), + message: _t("submit-tour.toolbar-hint"), + targetSelector: "#editor-toolbar" + }, + { + title: _t("submit-tour.title"), + message: _t("submit-tour.advanced-hint"), + targetSelector: "#editor-advanced" + }, + { + title: _t("submit-tour.title"), + message: _t("submit-tour.help-hint"), + targetSelector: "#editor-help" + } + ], + [] + ); let _updateTimer: any; // todo think about it @@ -381,10 +433,19 @@ export function Submit(props: PageProps & MatchProps) { {clearModal && setClearModal(false)} />} + + +
{editingEntry === null && activeUser && ( -
+
+ +
+ +
)}
)} -