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:
Thomas Camlong committed 2026-05-20 09:43:00 +02:00
1 parent 1279168538
commit 60bf867203
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>
);
};