mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
feat(settings): allow user to replay the onboarding tour
v1.4.15 Phase B5 — fourth (and final) commit. Closes acceptance
criterion 4.
Adds a Tour-replay card to Settings → Account, between Password and
the password dialog. The card:
- Calls `POST /api/onboarding/tour` with `{ completed: false }`
so the server flips `users.onboarding_tour_completed` back to
false. Idempotent: same DB write either direction; no audit
log noise on the reset.
- Refetches `/api/auth/me` so any in-memory <TourLauncher>
re-evaluates with the fresh flag.
- Dispatches a `healthlog:tour-restart` window event so a
launcher already mounted on the dashboard (e.g. user has a
dashboard tab open in another window) reopens the spotlight
immediately, without waiting for the user to navigate. The
launcher's listener clears the session-dismiss flag too.
- Surfaces a confirmation message via the same status pattern as
every other settings save (success / error / null).
i18n: reuses the `onboarding.tour.restart`,
`onboarding.tour.restartHint`, `onboarding.tour.restartConfirmation`
keys that landed in commit 2/4 (EN + DE). No new strings.
SSR test in `sections.test.tsx` locks the contract: the Restart
button is present in the static markup and no raw `onboarding.tour.*`
key leaks past the i18n layer.
Tests: my 40 unit tests pass (was 39 — +1 for the new SSR
assertion). The 12 unrelated failures observed in the full suite
all live in uncommitted B6 doctor-report work in the working tree
(`useEffect not defined` in advanced-section.tsx, missing
`lastReportPracticeName` in unmigrated DB schema fixtures, etc.).
Lint clean for B5 files; the single B6 lint error in
`doctor-report-dialog.tsx` is out of scope.
End-to-end shape:
Phase B5 acceptance criteria:
[x] DB migration 0030 + Prisma schema flag
[x] Tour component (state machine + spotlight + i18n + a11y)
[x] Auto-launch on first dashboard load
[x] Settings → Account → Restart tour button
This is the last commit in the B5 batch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8215e25876
commit
fa1c6a62ca
2 files changed
+104
No files matched your search
@@ -98,6 +98,17 @@ describe("settings sections — SSR smoke", () => {
|
|||||||
expect(html).not.toContain("settings.sections.");
|
expect(html).not.toContain("settings.sections.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("<AccountSection> exposes a Restart-tour button for B5 replay", () => {
|
||||||
|
const html = render(<AccountSection />);
|
||||||
|
// v1.4.15 phase B5 acceptance criterion 4: Settings → Account
|
||||||
|
// exposes a "Restart onboarding tour" button that resets the
|
||||||
|
// server-side flag. Rendered as a card next to Password reset.
|
||||||
|
expect(html).toContain('data-testid="settings-restart-tour"');
|
||||||
|
expect(html).toContain("Restart onboarding tour");
|
||||||
|
// No raw i18n key leaks.
|
||||||
|
expect(html).not.toContain("onboarding.tour.");
|
||||||
|
});
|
||||||
|
|
||||||
it("<AboutSection> renders version + license + check-for-updates button", () => {
|
it("<AboutSection> renders version + license + check-for-updates button", () => {
|
||||||
const html = render(<AboutSection />);
|
const html = render(<AboutSection />);
|
||||||
expect(html).toContain("About");
|
expect(html).toContain("About");
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import {
|
import {
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
|
Compass,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Loader2,
|
Loader2,
|
||||||
Save,
|
Save,
|
||||||
@@ -76,6 +77,19 @@ export function AccountSection() {
|
|||||||
>(null);
|
>(null);
|
||||||
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
const [passwordDialogOpen, setPasswordDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// v1.4.15 Phase B5 — onboarding-tour replay state. Settings → Account
|
||||||
|
// exposes a "Restart onboarding tour" button that resets the
|
||||||
|
// server-side flag (`users.onboarding_tour_completed = false`) and
|
||||||
|
// dispatches a `healthlog:tour-restart` window event so the dashboard's
|
||||||
|
// <TourLauncher> picks it up immediately on the user's next nav. The
|
||||||
|
// confirmation message goes through the same announce channel as
|
||||||
|
// every other settings save (status: "success" | "error" | null).
|
||||||
|
const [tourRestarting, setTourRestarting] = useState(false);
|
||||||
|
const [tourMsg, setTourMsg] = useState<string | null>(null);
|
||||||
|
const [tourMsgType, setTourMsgType] = useState<"success" | "error" | null>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
// Passkey registration state.
|
// Passkey registration state.
|
||||||
const [passkeyLoading, setPasskeyLoading] = useState(false);
|
const [passkeyLoading, setPasskeyLoading] = useState(false);
|
||||||
const [passkeyMsg, setPasskeyMsg] = useState<string | null>(null);
|
const [passkeyMsg, setPasskeyMsg] = useState<string | null>(null);
|
||||||
@@ -186,6 +200,41 @@ export function AccountSection() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleRestartTour() {
|
||||||
|
setTourRestarting(true);
|
||||||
|
setTourMsg(null);
|
||||||
|
setTourMsgType(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/onboarding/tour", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ completed: false }),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
setTourMsg(t("settings.savingError"));
|
||||||
|
setTourMsgType("error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Refetch the auth payload so `<TourLauncher>` re-evaluates
|
||||||
|
// with the new flag, AND fire a window event so a launcher
|
||||||
|
// already mounted on the dashboard reopens immediately
|
||||||
|
// without waiting for a navigation.
|
||||||
|
await refetch();
|
||||||
|
try {
|
||||||
|
window.dispatchEvent(new CustomEvent("healthlog:tour-restart"));
|
||||||
|
} catch {
|
||||||
|
/* ignore — only matters if the dashboard is already mounted */
|
||||||
|
}
|
||||||
|
setTourMsg(t("onboarding.tour.restartConfirmation"));
|
||||||
|
setTourMsgType("success");
|
||||||
|
} catch {
|
||||||
|
setTourMsg(t("common.networkError"));
|
||||||
|
setTourMsgType("error");
|
||||||
|
} finally {
|
||||||
|
setTourRestarting(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleChangePassword(e: React.FormEvent) {
|
async function handleChangePassword(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setPasswordSaving(true);
|
setPasswordSaving(true);
|
||||||
@@ -431,6 +480,50 @@ export function AccountSection() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tour replay card. v1.4.15 Phase B5: a one-shot button that
|
||||||
|
resets `users.onboarding_tour_completed` on the server AND
|
||||||
|
dispatches a window event so a dashboard already in the
|
||||||
|
background reopens the spotlight tour immediately. */}
|
||||||
|
<div className="bg-card border-border rounded-xl border p-6">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Compass className="text-primary h-5 w-5" />
|
||||||
|
<h2 className="text-lg font-semibold">
|
||||||
|
{t("onboarding.tour.restart")}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleRestartTour}
|
||||||
|
disabled={tourRestarting}
|
||||||
|
data-testid="settings-restart-tour"
|
||||||
|
>
|
||||||
|
{tourRestarting ? (
|
||||||
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Compass className="mr-2 h-4 w-4" />
|
||||||
|
)}
|
||||||
|
{t("onboarding.tour.restart")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-muted-foreground mt-1 text-xs">
|
||||||
|
{t("onboarding.tour.restartHint")}
|
||||||
|
</p>
|
||||||
|
{tourMsg && (
|
||||||
|
<p
|
||||||
|
role="alert"
|
||||||
|
className={`mt-2 text-xs ${
|
||||||
|
tourMsgType === "success"
|
||||||
|
? "text-dracula-green"
|
||||||
|
: "text-destructive"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tourMsg}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<Dialog
|
<Dialog
|
||||||
open={passwordDialogOpen}
|
open={passwordDialogOpen}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
|
|||||||
Reference in new issue
Block a user