mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
feat(onboarding): per-module tour re-entry + consolidated replay
Add a 'Diese Tour zeigen' affordance to every module header that opens the tour narrowed to that module's card in place, plus additive data-tour anchors (measurements/medications/labs/illness/vorsorge/cycle/mood/ insights/coach/integrations/export/achievements) so the cross-page walk has a real target on each page. Consolidate the full-tour replay onto a single Settings -> Advanced card, relabel it, and drop the duplicate from About. The replay reset clears the stored progress so it restarts cleanly.
This commit is contained in:
1 parent
f0c25cc275
commit
33fec28b29
24 files changed
+202
-210
No files matched your search
+1
-3
@@ -4621,9 +4621,7 @@
|
||||
"changelog": "Changelog",
|
||||
"docs": "Dokumentation",
|
||||
"linksHeading": "Quellen & Dokumentation",
|
||||
"newerAvailable": "Neue Version: {tag}",
|
||||
"tourReplay": "Tour ansehen",
|
||||
"tourReplayHint": "Zeige die Dashboard-Tour — praktisch, falls du sie beim ersten Besuch übersprungen hast."
|
||||
"newerAvailable": "Neue Version: {tag}"
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Verbindung testen",
|
||||
|
||||
+1
-3
@@ -4621,9 +4621,7 @@
|
||||
"changelog": "Changelog",
|
||||
"docs": "Documentation",
|
||||
"linksHeading": "Sources & docs",
|
||||
"newerAvailable": "Newer version: {tag}",
|
||||
"tourReplay": "Replay the tour",
|
||||
"tourReplayHint": "Watch the dashboard walkthrough — handy if you skipped it on first visit."
|
||||
"newerAvailable": "Newer version: {tag}"
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Test connection",
|
||||
|
||||
+1
-3
@@ -4621,9 +4621,7 @@
|
||||
"changelog": "Changelog",
|
||||
"docs": "Documentación",
|
||||
"linksHeading": "Fuentes y documentación",
|
||||
"newerAvailable": "Nueva versión: {tag}",
|
||||
"tourReplay": "Volver a ver el recorrido",
|
||||
"tourReplayHint": "Mira el recorrido del panel — útil si lo saltaste en la primera visita."
|
||||
"newerAvailable": "Nueva versión: {tag}"
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Probar conexión",
|
||||
|
||||
+1
-3
@@ -4621,9 +4621,7 @@
|
||||
"changelog": "Changelog",
|
||||
"docs": "Documentation",
|
||||
"linksHeading": "Sources et documentation",
|
||||
"newerAvailable": "Nouvelle version : {tag}",
|
||||
"tourReplay": "Revoir la visite",
|
||||
"tourReplayHint": "Voir la visite guidée du tableau de bord — pratique si tu l'as sautée à la première visite."
|
||||
"newerAvailable": "Nouvelle version : {tag}"
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Tester la connexion",
|
||||
|
||||
+1
-3
@@ -4621,9 +4621,7 @@
|
||||
"changelog": "Changelog",
|
||||
"docs": "Documentazione",
|
||||
"linksHeading": "Fonti e documentazione",
|
||||
"newerAvailable": "Nuova versione: {tag}",
|
||||
"tourReplay": "Rivedi il tour",
|
||||
"tourReplayHint": "Guarda il tour della dashboard — utile se l'hai saltato alla prima visita."
|
||||
"newerAvailable": "Nuova versione: {tag}"
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Prova connessione",
|
||||
|
||||
+1
-3
@@ -4621,9 +4621,7 @@
|
||||
"changelog": "Changelog",
|
||||
"docs": "Dokumentacja",
|
||||
"linksHeading": "Źródła i dokumentacja",
|
||||
"newerAvailable": "Nowa wersja: {tag}",
|
||||
"tourReplay": "Obejrzyj samouczek",
|
||||
"tourReplayHint": "Zobacz przewodnik po panelu — przydatne, jeśli pominąłeś go przy pierwszej wizycie."
|
||||
"newerAvailable": "Nowa wersja: {tag}"
|
||||
},
|
||||
"testConnection": {
|
||||
"test": "Testuj połączenie",
|
||||
|
||||
@@ -34,6 +34,7 @@ import { Badge } from "@/components/ui/badge";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { useAchievementsQuery } from "@/lib/queries/use-achievements-query";
|
||||
import {
|
||||
@@ -317,13 +318,21 @@ export default function AchievementsPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
{t("achievements.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("achievements.subtitle")}
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h1
|
||||
data-tour-id="achievements-hero"
|
||||
className="text-2xl font-bold tracking-tight"
|
||||
>
|
||||
{t("achievements.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("achievements.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ModuleTourTrigger stopId="achievements" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
|
||||
@@ -194,9 +194,9 @@ export function AdminSectionRenderer({
|
||||
// v1.4.36 W4e — About section reused as-is from the settings
|
||||
// surface. The component owns its own heading + cards layout
|
||||
// so no SectionFrame wrapper.
|
||||
// v1.18.1 E5 — hide the onboarding "Replay tour" card on the admin
|
||||
// surface; it's a user-facing affordance with no admin-side use.
|
||||
return <AboutSection hideTourReplay />;
|
||||
// v1.18.6 — the About surface no longer carries the tour-replay card
|
||||
// (consolidated onto Settings → Advanced), so there's nothing to hide.
|
||||
return <AboutSection />;
|
||||
default:
|
||||
slug satisfies never;
|
||||
return null;
|
||||
|
||||
+17
-12
@@ -6,6 +6,7 @@ import { Minimize2 } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CoachConversation } from "@/components/insights/coach-panel/coach-conversation";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { useCoachLaunch } from "@/lib/insights/coach-launch-context";
|
||||
import { useFeatureFlags } from "@/hooks/use-feature-flags";
|
||||
@@ -61,6 +62,7 @@ export default function CoachPage() {
|
||||
return (
|
||||
<div
|
||||
data-slot="coach-page"
|
||||
data-tour-id="coach-hero"
|
||||
// v1.18.1 (W-COACH-UI C1/C3) — the card was capped at
|
||||
// `100dvh-13rem`, leaving a band of wasted space below it on a
|
||||
// standalone route that no longer carries the Insights tab strip.
|
||||
@@ -77,18 +79,21 @@ export default function CoachPage() {
|
||||
<h1 className="min-w-0 truncate text-sm font-semibold">{title}</h1>
|
||||
)}
|
||||
leadingHeaderActions={
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleMinimize}
|
||||
data-slot="coach-page-minimize"
|
||||
aria-label={t("insights.coach.minimizeAriaLabel")}
|
||||
title={t("insights.coach.minimizeAriaLabel")}
|
||||
className="text-muted-foreground hover:text-foreground size-11 shrink-0"
|
||||
>
|
||||
<Minimize2 className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
<>
|
||||
<ModuleTourTrigger stopId="coach" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={handleMinimize}
|
||||
data-slot="coach-page-minimize"
|
||||
aria-label={t("insights.coach.minimizeAriaLabel")}
|
||||
title={t("insights.coach.minimizeAriaLabel")}
|
||||
className="text-muted-foreground hover:text-foreground size-11 shrink-0"
|
||||
>
|
||||
<Minimize2 className="size-4" aria-hidden="true" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,7 @@ import { useMounted } from "@/hooks/use-mounted";
|
||||
import { usePullToRefresh } from "@/hooks/use-pull-to-refresh";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
|
||||
export default function LabsPage() {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
@@ -63,7 +64,10 @@ export default function LabsPage() {
|
||||
<PullToRefreshIndicator {...pull} />
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
<h1
|
||||
data-tour-id="labs-hero"
|
||||
className="text-2xl font-bold tracking-tight"
|
||||
>
|
||||
{t("labs.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-xs sm:text-sm">
|
||||
@@ -71,6 +75,7 @@ export default function LabsPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ModuleTourTrigger stopId="labs" />
|
||||
<Button
|
||||
onClick={() => setDialogOpen(true)}
|
||||
className="min-h-11 sm:min-h-9"
|
||||
|
||||
@@ -16,6 +16,7 @@ import { ResponsiveSheet } from "@/components/ui/responsive-sheet";
|
||||
import { Plus, Loader2 } from "lucide-react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
|
||||
export default function MeasurementsPage() {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
@@ -110,7 +111,10 @@ export default function MeasurementsPage() {
|
||||
<PullToRefreshIndicator {...pull} />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
<h1
|
||||
data-tour-id="measurements-hero"
|
||||
className="text-2xl font-bold tracking-tight"
|
||||
>
|
||||
{t("measurements.title")}
|
||||
</h1>
|
||||
{/* v1.4.34 IW-G — subtitle now stays visible on mobile so
|
||||
@@ -120,13 +124,16 @@ export default function MeasurementsPage() {
|
||||
{t("measurements.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className="min-h-11 shrink-0 sm:min-h-9"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("measurements.addMeasurement")}
|
||||
</Button>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ModuleTourTrigger stopId="measurements" />
|
||||
<Button
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => setDialogOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("measurements.addMeasurement")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ResponsiveSheet
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
import { MedicationWizardDialog } from "@/components/medications/wizard/MedicationWizardDialog";
|
||||
import { MedicationCard } from "@/components/medications/medication-card";
|
||||
import { Glp1MedicationCard } from "@/components/medications/glp1-medication-card";
|
||||
@@ -328,7 +329,10 @@ export default function MedicationsPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 space-y-1">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
<h1
|
||||
data-tour-id="medications-hero"
|
||||
className="text-2xl font-bold tracking-tight"
|
||||
>
|
||||
{t("medications.title")}
|
||||
</h1>
|
||||
{/* v1.4.34 IW-G — subtitle stays visible on mobile so the
|
||||
@@ -338,6 +342,7 @@ export default function MedicationsPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ModuleTourTrigger stopId="medications" />
|
||||
{/* v1.16.11 (#316) — "Alle fälligen einnehmen". Contextual, not a
|
||||
permanent fixture: it renders only while at least TWO
|
||||
medications are currently due (a single due dose is the
|
||||
|
||||
@@ -15,6 +15,7 @@ import { Plus, Loader2, Wrench } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
|
||||
export default function MoodPage() {
|
||||
const { isAuthenticated, isLoading } = useAuth();
|
||||
@@ -65,7 +66,10 @@ export default function MoodPage() {
|
||||
<PullToRefreshIndicator {...pull} />
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
<h1
|
||||
data-tour-id="mood-hero"
|
||||
className="text-2xl font-bold tracking-tight"
|
||||
>
|
||||
{t("mood.title")}
|
||||
</h1>
|
||||
{/* v1.4.34 IW-G — subtitle stays visible on mobile so the
|
||||
@@ -75,6 +79,7 @@ export default function MoodPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ModuleTourTrigger stopId="mood" />
|
||||
{/* v1.17 — the wrench is the one customize entry point: it
|
||||
links to /settings/mood, which owns the tag groups, custom
|
||||
tags, visibility, and picker order. Same glyph, slot (left
|
||||
|
||||
@@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { CycleRing } from "./cycle-ring";
|
||||
import { BbtChart } from "./bbt-chart";
|
||||
@@ -139,20 +140,26 @@ export function CycleView() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
<h1
|
||||
data-tour-id="cycle-hero"
|
||||
className="text-2xl font-bold tracking-tight"
|
||||
>
|
||||
{t("cycle.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-xs sm:text-sm">
|
||||
{t("cycle.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => openSheet(today)}
|
||||
className="min-h-11 shrink-0 sm:min-h-9"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("cycle.logToday")}
|
||||
</Button>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ModuleTourTrigger stopId="cycle" />
|
||||
<Button
|
||||
onClick={() => openSheet(today)}
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("cycle.logToday")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Desktop: ring (left) + tabs (right). Single column below lg. */}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useMounted } from "@/hooks/use-mounted";
|
||||
import { getHourForTimeZone } from "@/components/dashboard/range-display";
|
||||
@@ -96,6 +97,8 @@ export function DashboardHeader({
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{/* v1.18.6 — re-enter the guided module tour from the dashboard. */}
|
||||
<ModuleTourTrigger stopId="dashboardOverview" />
|
||||
{/* Customize shortcut to the dashboard-customization settings
|
||||
(the tile/layout editor at /settings/dashboard). Sits to
|
||||
the left of the add button as a monochrome ghost icon, with
|
||||
|
||||
@@ -32,6 +32,7 @@ import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
|
||||
import { LogDaySheet } from "./log-day-sheet";
|
||||
import { NewEpisodeSheet } from "./new-episode-sheet";
|
||||
@@ -256,20 +257,26 @@ export function IllnessView() {
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<h1 className="text-2xl font-bold tracking-tight">
|
||||
<h1
|
||||
data-tour-id="illness-hero"
|
||||
className="text-2xl font-bold tracking-tight"
|
||||
>
|
||||
{t("illness.title")}
|
||||
</h1>
|
||||
<p className="text-muted-foreground text-xs sm:text-sm">
|
||||
{t("illness.subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => setNewOpen(true)}
|
||||
className="min-h-11 shrink-0 sm:min-h-9"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("illness.newEpisode")}
|
||||
</Button>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ModuleTourTrigger stopId="illness" />
|
||||
<Button
|
||||
onClick={() => setNewOpen(true)}
|
||||
className="min-h-11 sm:min-h-9"
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
{t("illness.newEpisode")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-muted-foreground text-xs">{t("illness.disclaimer")}</p>
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { prefersReducedMotion } from "@/lib/charts/reduced-motion";
|
||||
import {
|
||||
@@ -656,6 +657,7 @@ function InsightsTabStripImpl({
|
||||
return (
|
||||
<nav
|
||||
data-slot="insights-tab-strip"
|
||||
data-tour-id="insights-hero"
|
||||
aria-label={t("insights.navAriaLabel")}
|
||||
// v1.4.28 FB-D3 — `touch-action: pan-y` lets vertical swipes that
|
||||
// begin on the sticky strip scroll the page through it. The
|
||||
@@ -858,6 +860,10 @@ function InsightsTabStripImpl({
|
||||
"from-background/95 pointer-events-none absolute inset-y-0 right-0 w-8 bg-gradient-to-l to-transparent sm:hidden",
|
||||
)}
|
||||
/>
|
||||
{/* v1.18.6 — per-module tour re-entry, travelling with the
|
||||
top-right control cluster. Anchors to this strip's
|
||||
`insights-hero` and opens the Insights tour card on the spot. */}
|
||||
<ModuleTourTrigger stopId="insights" />
|
||||
{/* v1.15.18 — customise cog. Sits immediately LEFT of the
|
||||
regenerate button and links to the Insights settings section
|
||||
(overview arrange + pill sort), matching the Dashboard cog
|
||||
|
||||
@@ -32,6 +32,7 @@ import { useState } from "react";
|
||||
import { CalendarClock, CheckCircle2, Plus, Settings2 } from "lucide-react";
|
||||
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -370,6 +371,7 @@ export function VorsorgeSection({
|
||||
<div className="min-w-0">
|
||||
<h1
|
||||
id="vorsorge-section-title"
|
||||
data-tour-id="vorsorge-hero"
|
||||
className="text-2xl font-bold tracking-tight"
|
||||
>
|
||||
{t("measurementReminders.sectionTitle")}
|
||||
@@ -379,6 +381,7 @@ export function VorsorgeSection({
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<ModuleTourTrigger stopId="vorsorge" />
|
||||
{addButton}
|
||||
{wrenchButton}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.18.6 — per-module "Diese Tour zeigen" re-entry.
|
||||
*
|
||||
* A small ghost button each module page drops into its header action
|
||||
* cluster. It dispatches the `healthlog:module-tour` window event with
|
||||
* the page's stop id; the shell-level `<TourLauncher>` picks it up and
|
||||
* opens the spotlight narrowed to that single module card on the
|
||||
* current page (no cross-page navigation, no completion flip).
|
||||
*
|
||||
* Anchors to the page's `data-tour="<…>-hero"` element via the launcher
|
||||
* → overlay path. The button itself carries no anchor — it only fires
|
||||
* the event.
|
||||
*/
|
||||
|
||||
import { Compass } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import type { TourStopId } from "@/lib/onboarding/tour-state";
|
||||
|
||||
export function ModuleTourTrigger({ stopId }: { stopId: TourStopId }) {
|
||||
const { t } = useTranslations();
|
||||
|
||||
function show() {
|
||||
if (typeof window === "undefined") return;
|
||||
try {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("healthlog:module-tour", { detail: { stopId } }),
|
||||
);
|
||||
} catch {
|
||||
/* ignore — sandboxed iframes etc. */
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={show}
|
||||
// 44px mobile tap floor, matching the rest of the module headers.
|
||||
className="text-muted-foreground hover:text-foreground min-h-11 sm:min-h-9"
|
||||
data-testid={`module-tour-trigger-${stopId}`}
|
||||
>
|
||||
<Compass className="h-4 w-4" />
|
||||
<span className="sr-only sm:not-sr-only">
|
||||
{t("onboarding.tour.moduleTrigger")}
|
||||
</span>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -119,12 +119,13 @@ describe("settings sections — SSR smoke", () => {
|
||||
expect(html).not.toContain("settings.sections.");
|
||||
});
|
||||
|
||||
it("<AdvancedSection> exposes the Restart-tour button (moved from Account)", () => {
|
||||
// v1.18.1 (D1) — the "Restart onboarding tour" card moved from Settings →
|
||||
// Account into Settings → Advanced (a maintenance/reset action).
|
||||
it("<AdvancedSection> exposes the single Restart-tour card", () => {
|
||||
// v1.18.1 (D1) — the tour-replay card moved from Account into Advanced.
|
||||
// v1.18.6 — relabelled "Restart module tour" and consolidated as the one
|
||||
// full-tour replay home (the About duplicate was removed).
|
||||
const html = render(<AdvancedSection />);
|
||||
expect(html).toContain('data-testid="settings-restart-tour"');
|
||||
expect(html).toContain("Restart onboarding tour");
|
||||
expect(html).toContain("Restart module tour");
|
||||
// No raw i18n key leaks.
|
||||
expect(html).not.toContain("onboarding.tour.");
|
||||
});
|
||||
@@ -148,27 +149,13 @@ describe("settings sections — SSR smoke", () => {
|
||||
expect(html).not.toContain("settings.about.");
|
||||
});
|
||||
|
||||
it("<AboutSection> exposes a Replay-tour button for the W5 chain gate", () => {
|
||||
it("<AboutSection> no longer carries a tour-replay card (consolidated to Advanced)", () => {
|
||||
// v1.18.6 — the tour replay lives on one home in Settings → Advanced;
|
||||
// the About duplicate was removed. Per-module "Diese Tour zeigen"
|
||||
// affordances cover in-context re-entry.
|
||||
const html = render(<AboutSection />);
|
||||
// v1.4.47 W5 — the spotlight tour now auto-launches only ≥ 24 h
|
||||
// after the wizard finishes (see `shouldAutoLaunchTour` in
|
||||
// `components/onboarding/tour-launcher.tsx`). A first-day user who
|
||||
// *does* want the tour needs a discoverable manual trigger, and
|
||||
// Settings → About is the surface every user can find. Pin both
|
||||
// the data-testid and the visible copy so a future refactor can't
|
||||
// silently drop the button.
|
||||
expect(html).toContain('data-testid="about-replay-tour"');
|
||||
expect(html).toContain("Replay the tour");
|
||||
// No raw i18n key leaks past the i18n layer.
|
||||
expect(html).not.toContain("settings.about.tourReplay");
|
||||
});
|
||||
|
||||
it("<AboutSection> renders the German Replay-tour label end-to-end", () => {
|
||||
// project-voice + i18n parity: the W5 spec explicitly names this
|
||||
// surface "Tour ansehen" in German. Lock the resolved copy so a
|
||||
// future locale-bundle drift doesn't silently fall back to English.
|
||||
const html = render(<AboutSection />, "de");
|
||||
expect(html).toContain("Tour ansehen");
|
||||
expect(html).not.toContain('data-testid="about-replay-tour"');
|
||||
expect(html).not.toContain("Replay the tour");
|
||||
});
|
||||
|
||||
it("<AiSection> renders the AI Insights heading", () => {
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowUpCircle,
|
||||
BookOpen,
|
||||
Compass,
|
||||
ExternalLink,
|
||||
GitBranch,
|
||||
Info,
|
||||
@@ -15,9 +14,7 @@ import {
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
import { useFormatters, useTranslations } from "@/lib/i18n/context";
|
||||
import { restartOnboardingTour } from "@/lib/onboarding/tour-restart";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import { apiFetchRaw, apiGet } from "@/lib/api/api-fetch";
|
||||
|
||||
@@ -118,25 +115,9 @@ export function UpdateBadge({
|
||||
);
|
||||
}
|
||||
|
||||
export interface AboutSectionProps {
|
||||
/**
|
||||
* v1.18.1 E5 — suppress the "Replay tour" card. The admin → Über
|
||||
* surface reuses this component but the onboarding tour is a
|
||||
* user-facing affordance with no place in the admin console, so the
|
||||
* admin renderer passes `hideTourReplay`. The Settings → About surface
|
||||
* leaves it on.
|
||||
*/
|
||||
hideTourReplay?: boolean;
|
||||
}
|
||||
|
||||
export function AboutSection({ hideTourReplay = false }: AboutSectionProps = {}) {
|
||||
export function AboutSection() {
|
||||
const { t } = useTranslations();
|
||||
const fmt = useFormatters();
|
||||
// Pull the auth user — only needed so the "Replay the tour" button
|
||||
// can stamp a per-user force-launch marker into sessionStorage. The
|
||||
// hook is cheap (a cached `/api/auth/me` read) and every settings
|
||||
// page already lives behind the same auth boundary.
|
||||
const { user } = useAuth();
|
||||
|
||||
const { data: version, isLoading } = useQuery({
|
||||
queryKey: queryKeys.apiVersion(),
|
||||
@@ -147,45 +128,6 @@ export function AboutSection({ hideTourReplay = false }: AboutSectionProps = {})
|
||||
staleTime: Infinity,
|
||||
});
|
||||
|
||||
// v1.4.47 W5 — onboarding chain-gate: the spotlight tour now auto-
|
||||
// launches only ≥ 24 h after the wizard finishes (see
|
||||
// `shouldAutoLaunchTour` in `components/onboarding/tour-launcher.tsx`).
|
||||
// The carousel + tour no longer stack into ~90 s of forced onboarding
|
||||
// on first visit. The tradeoff: a first-day user who *does* want the
|
||||
// tour needs a discoverable manual trigger. Settings → About is the
|
||||
// surface every user can find (every locale puts "About" right on the
|
||||
// settings shell), so it carries the replay button next to the
|
||||
// sources/docs links. Settings → Account still has its own
|
||||
// "Restart onboarding tour" — both buttons share the same flow.
|
||||
const [replayingTour, setReplayingTour] = useState(false);
|
||||
// v1.4.48 M6c — text + type collapsed into one discriminated state so
|
||||
// the two values can never drift (a "success" message rendered in the
|
||||
// destructive colour was the latent bug class).
|
||||
const [tourFeedback, setTourFeedback] = useState<{
|
||||
text: string;
|
||||
type: "success" | "error";
|
||||
} | null>(null);
|
||||
|
||||
async function handleReplayTour() {
|
||||
setReplayingTour(true);
|
||||
setTourFeedback(null);
|
||||
// v1.4.48 M6b — both Settings → Account and Settings → About now
|
||||
// delegate to the shared `restartOnboardingTour()` worker so the
|
||||
// server flip + force-launch marker + window event live in one
|
||||
// place. The helper returns a discriminated result; this surface
|
||||
// only owns the translation + feedback rendering.
|
||||
const result = await restartOnboardingTour(user?.id);
|
||||
if (result.ok) {
|
||||
setTourFeedback({
|
||||
text: t("onboarding.tour.restartConfirmation"),
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
setTourFeedback({ text: t(result.messageKey), type: "error" });
|
||||
}
|
||||
setReplayingTour(false);
|
||||
}
|
||||
|
||||
// v1.4.36 W4f — the explicit "Check for updates" button is gone;
|
||||
// only the 24 h auto-check stays. When the auto-check reports
|
||||
// `newer_available`, a subtle upward-arrow badge appears next to
|
||||
@@ -364,63 +306,10 @@ export function AboutSection({ hideTourReplay = false }: AboutSectionProps = {})
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* v1.4.47 W5 — Replay tour card. The dashboard spotlight
|
||||
tour now auto-launches only ≥ 24 h after the wizard finishes,
|
||||
so a first-day user who wants to see it still needs a
|
||||
discoverable manual trigger. About is the surface every user
|
||||
can find (it's the standard "where am I?" stop in the
|
||||
settings shell), so the button lives here next to version +
|
||||
links. Mirrors the stack-on-mobile / right-align-on-desktop
|
||||
contract used by the Account section's "Restart onboarding
|
||||
tour" card so the two surfaces feel consistent.
|
||||
|
||||
v1.18.1 E5 — hidden on the admin → Über surface (`hideTourReplay`):
|
||||
the onboarding tour is a user-facing affordance with no place in
|
||||
the admin console. */}
|
||||
{!hideTourReplay && (
|
||||
<div className="bg-card border-border rounded-xl border p-4 sm:p-6">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between sm:gap-2">
|
||||
<div className="flex items-start gap-2">
|
||||
<Compass className="text-muted-foreground mt-0.5 h-5 w-5 shrink-0" />
|
||||
<div className="space-y-1">
|
||||
<h2 className="text-lg font-semibold">
|
||||
{t("settings.about.tourReplay")}
|
||||
</h2>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.about.tourReplayHint")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={handleReplayTour}
|
||||
disabled={replayingTour}
|
||||
data-testid="about-replay-tour"
|
||||
className="w-full shrink-0 sm:w-auto"
|
||||
>
|
||||
{replayingTour ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Compass className="h-4 w-4" />
|
||||
)}
|
||||
{t("settings.about.tourReplay")}
|
||||
</Button>
|
||||
</div>
|
||||
{tourFeedback && (
|
||||
<p
|
||||
role="alert"
|
||||
className={`mt-2 pl-7 text-xs ${
|
||||
tourFeedback.type === "success"
|
||||
? "text-success"
|
||||
: "text-destructive"
|
||||
}`}
|
||||
>
|
||||
{tourFeedback.text}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{/* v1.18.6 — the tour-replay card consolidated onto a single home in
|
||||
Settings → Advanced ("Modul-Tour neu starten"). The About surface
|
||||
no longer duplicates it; per-module "Diese Tour zeigen" affordances
|
||||
on every module page cover in-context re-entry. */}
|
||||
|
||||
{/* v1.4.36 W4f — the dedicated "Updates" card with the
|
||||
manual "Check for updates" button is gone. The 24 h auto-
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
import { HealthRecordExportPanel } from "@/components/settings/health-record-export-panel";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
|
||||
export function GesundheitsakteSection() {
|
||||
const { t } = useTranslations();
|
||||
@@ -28,13 +29,18 @@ export function GesundheitsakteSection() {
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
<header>
|
||||
<header className="flex items-center justify-end">
|
||||
<h1
|
||||
id="settings-section-gesundheitsakte-title"
|
||||
className="sr-only"
|
||||
>
|
||||
{t("settings.sections.gesundheitsakte.title")}
|
||||
</h1>
|
||||
{/* v1.18.6 — guided-tour re-entry for the doctor-report module + a
|
||||
real spotlight anchor (the section h1 is sr-only). */}
|
||||
<span data-tour-id="export-hero">
|
||||
<ModuleTourTrigger stopId="export" />
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<HealthRecordExportPanel />
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { ModuleTourTrigger } from "@/components/onboarding/module-tour-trigger";
|
||||
|
||||
export {
|
||||
parseOAuthOutcome,
|
||||
@@ -32,10 +33,16 @@ export function IntegrationsSection() {
|
||||
className="space-y-6"
|
||||
>
|
||||
{/* v1.18.1 (D0) — section blurb dropped for consistent top alignment. */}
|
||||
<header>
|
||||
<header className="flex items-center justify-end">
|
||||
<h1 id="settings-section-integrations-title" className="sr-only">
|
||||
{t("settings.sections.integrations.title")}
|
||||
</h1>
|
||||
{/* v1.18.6 — guided-tour re-entry for the integrations module. The
|
||||
visible affordance also gives the spotlight a real anchor (the
|
||||
section h1 is sr-only). */}
|
||||
<span data-tour-id="integrations-hero">
|
||||
<ModuleTourTrigger stopId="integrations" />
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<ConnectionsPanel />
|
||||
|
||||
@@ -189,7 +189,7 @@ const ALL_STOPS: readonly TourStop[] = [
|
||||
id: "export",
|
||||
targetId: "export-hero",
|
||||
placement: "bottom",
|
||||
route: "/settings/advanced",
|
||||
route: "/settings/gesundheitsakte",
|
||||
titleKey: "onboarding.tour.steps.export.title",
|
||||
bodyKey: "onboarding.tour.steps.export.body",
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user