feat(onboarding): add board tour on dashboard pages
Scope the board tour to content pages, target header actions, remove the loading overlay, and persist completion with optimistic cache updates.
This commit is contained in:
5 files changed
+78
-64
No files matched your search
@@ -3,26 +3,35 @@
|
||||
import type { PropsWithChildren } from "react";
|
||||
import { useMemo } from "react";
|
||||
import type { OnboardingTourStep } from "@gfazioli/mantine-onboarding-tour";
|
||||
import { OnboardingTour } from "@gfazioli/mantine-onboarding-tour";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { createDocumentationLink } from "@homarr/definitions";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { useBoardPermissions } from "~/components/board/permissions/client";
|
||||
import { SkipHoldButton } from "./skip-hold-button";
|
||||
import { TourShell } from "./tour-shell";
|
||||
import { TourStepContent } from "./tour-step-content";
|
||||
|
||||
export const BoardTourProvider = ({ children }: PropsWithChildren) => {
|
||||
const t = useScopedI18n("onboardingTour.board");
|
||||
const board = useRequiredBoard();
|
||||
const { hasChangeAccess } = useBoardPermissions(board);
|
||||
const utils = clientApi.useUtils();
|
||||
const { data: tourStatus } = clientApi.user.getTourStatus.useQuery();
|
||||
const { mutate: completeTour } = clientApi.user.completeTour.useMutation();
|
||||
const { mutate: completeTour } = clientApi.user.completeTour.useMutation({
|
||||
onSuccess() {
|
||||
void utils.user.getTourStatus.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const started = tourStatus !== undefined && !tourStatus.completedBoardTour;
|
||||
|
||||
const handleEnd = () => {
|
||||
utils.user.getTourStatus.setData(undefined, {
|
||||
completedManageTour: tourStatus?.completedManageTour ?? false,
|
||||
completedBoardTour: true,
|
||||
});
|
||||
completeTour({ tour: "board" });
|
||||
};
|
||||
|
||||
@@ -34,13 +43,12 @@ export const BoardTourProvider = ({ children }: PropsWithChildren) => {
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("welcome.description")}
|
||||
docPath="/docs/getting-started/after-the-installation"
|
||||
docHash="#creating-your-first-board"
|
||||
documentationHref={createDocumentationLink(
|
||||
"/docs/getting-started/after-the-installation",
|
||||
"#creating-your-first-board",
|
||||
)}
|
||||
/>
|
||||
),
|
||||
footer: (controller: { skipTour: () => void }) => (
|
||||
<SkipHoldButton onSkip={() => controller.skipTour()} />
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "board-edit-mode",
|
||||
@@ -49,8 +57,10 @@ export const BoardTourProvider = ({ children }: PropsWithChildren) => {
|
||||
content: (
|
||||
<TourStepContent
|
||||
description={t("editMode.description")}
|
||||
docPath="/docs/getting-started/after-the-installation"
|
||||
docHash="#arrange-and-organize-your-board"
|
||||
documentationHref={createDocumentationLink(
|
||||
"/docs/getting-started/after-the-installation",
|
||||
"#arrange-and-organize-your-board",
|
||||
)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
@@ -59,39 +69,40 @@ export const BoardTourProvider = ({ children }: PropsWithChildren) => {
|
||||
title: t("settings.title"),
|
||||
requiresChangeAccess: true,
|
||||
content: (
|
||||
<TourStepContent description={t("settings.description")} docPath="/docs/management/boards" />
|
||||
<TourStepContent
|
||||
description={t("settings.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/boards")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "board-switcher",
|
||||
title: t("switcher.title"),
|
||||
content: (
|
||||
<TourStepContent description={t("switcher.description")} docPath="/docs/management/boards" />
|
||||
<TourStepContent
|
||||
description={t("switcher.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/boards")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "board-user-menu",
|
||||
title: t("userMenu.title"),
|
||||
content: (
|
||||
<TourStepContent description={t("userMenu.description")} docPath="/docs/management/users" />
|
||||
<TourStepContent
|
||||
description={t("userMenu.description")}
|
||||
documentationHref={createDocumentationLink("/docs/management/users")}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return allSteps.filter(
|
||||
(step) => !step.requiresChangeAccess || hasChangeAccess,
|
||||
) as OnboardingTourStep[];
|
||||
return allSteps.filter((step) => !step.requiresChangeAccess || hasChangeAccess) as OnboardingTourStep[];
|
||||
}, [hasChangeAccess, t]);
|
||||
|
||||
return (
|
||||
<OnboardingTour
|
||||
tour={steps}
|
||||
started={started}
|
||||
onOnboardingTourComplete={handleEnd}
|
||||
onOnboardingTourSkip={handleEnd}
|
||||
maw={420}
|
||||
>
|
||||
<TourShell steps={steps} started={started} onEnd={handleEnd} position={{ base: "bottom", sm: "left" }}>
|
||||
{children}
|
||||
</OnboardingTour>
|
||||
</TourShell>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user