From 0729c2028b281da62164858625038115c1630b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 16 May 2026 20:48:50 +0200 Subject: [PATCH] feat(dashboard): hoist Coach launch provider and add hero CTA MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Coach drawer was previously only reachable from `/insights/**`. The `` + `` now live on the global `` so every authenticated route shares the same launch context, and the dashboard hero strip carries a "Frag den Coach" / "Ask the coach" CTA next to the existing "Hinzufügen" pill. Provider hoist - `` wraps the authed branch in `src/components/layout/auth-shell.tsx`; `` sits beneath the main scroller so the drawer renders once at the shell level. - `src/app/insights/layout.tsx` drops the provider + drawer mount. The FAB stays scoped to `/insights/**` because the floating action would distract from the dashboard hero on surfaces with a contextual inline CTA. Dashboard hero CTA - New ` - - - {/* Menu items must each carry a self-contained verb-phrase - ("Log measurement", "Log mood") — the trigger above already - says "Add", and the icon is `aria-hidden`, so the visible - text is the only thing distinguishing the rows. v1.4.15 - phase-A3 fix #1 hardened this with a unit guard at - `src/app/__tests__/quick-add-labels.test.ts` — both labels - must differ from each other AND from `common.add`. */} - setQuickEntryDialog("measurement")} - > - - setQuickEntryDialog("mood")}> - - - + ) : null} + + + {/* v1.4.33 maintainer-item-7 — restore proportional sizing + across viewports. The v1.4.27 fix pinned a `size="sm" + min-h-11` combo to hit WCAG 2.5.5's 44 px touch-target + contract on mobile, but `size="sm"` is h-8 (32 px) and + the `min-h-11` override stretched the cap vertically + while keeping the small horizontal padding — the + button read as klobig on Pixel 5. Switch to + `size="default"` (h-10 = 40 px) on mobile with a + responsive `min-h-11 sm:min-h-9` so the button is + 44 px tall under finger pressure and shrinks back to + the desktop-friendly 36 px on `sm:` upwards. The icon + + label keep the same visual contract. */} + + + + {/* Menu items must each carry a self-contained verb-phrase + ("Log measurement", "Log mood") — the trigger above already + says "Add", and the icon is `aria-hidden`, so the visible + text is the only thing distinguishing the rows. v1.4.15 + phase-A3 fix #1 hardened this with a unit guard at + `src/app/__tests__/quick-add-labels.test.ts` — both labels + must differ from each other AND from `common.add`. */} + setQuickEntryDialog("measurement")} + > + + setQuickEntryDialog("mood")}> + + + + {/* v1.4: Getting-started checklist for brand-new users. diff --git a/src/components/layout/__tests__/auth-shell-coach-hoist.test.ts b/src/components/layout/__tests__/auth-shell-coach-hoist.test.ts new file mode 100644 index 000000000..fbe85e1b3 --- /dev/null +++ b/src/components/layout/__tests__/auth-shell-coach-hoist.test.ts @@ -0,0 +1,62 @@ +/** + * v1.4.34 IW-B — auth-shell hoist contract. + * + * The Coach launch provider used to live on `app/insights/layout.tsx`, + * which meant the drawer was only reachable from `/insights/**`. The + * hoist moves the provider + the drawer mount to `` so every + * authenticated route (dashboard included) shares the same context and + * can call `askCoach()` from any descendant. + * + * Project convention keeps this contract pinned at the source level + * because `` is hard to render under SSR (it reads + * `useAuth()` + `usePathname()` + the router) and we don't want a fully + * mocked `@testing-library/react` setup for a one-line invariant. + */ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; + +import { describe, expect, it } from "vitest"; + +const AUTH_SHELL_PATH = resolve( + __dirname, + "..", + "auth-shell.tsx", +); +const INSIGHTS_LAYOUT_PATH = resolve( + __dirname, + "..", + "..", + "..", + "app", + "insights", + "layout.tsx", +); + +describe("v1.4.34 IW-B — CoachLaunchProvider hoist", () => { + it("imports the CoachLaunchProvider in the auth-shell", () => { + const source = readFileSync(AUTH_SHELL_PATH, "utf8"); + expect(source).toContain( + 'from "@/lib/insights/coach-launch-context"', + ); + expect(source).toContain("CoachLaunchProvider"); + }); + + it("mounts the LayoutCoachMount inside the auth-shell so the drawer is reachable globally", () => { + const source = readFileSync(AUTH_SHELL_PATH, "utf8"); + expect(source).toContain(""); + }); + + it("no longer mounts the provider in the routed insights layout", () => { + const source = readFileSync(INSIGHTS_LAYOUT_PATH, "utf8"); + // The provider import drops; the FAB import stays because the FAB + // is still scoped to `/insights/**` only. + expect(source).not.toContain( + 'from "@/lib/insights/coach-launch-context"', + ); + // The drawer mount lives on the shell now — verify the routed + // layout doesn't double-mount it. + expect(source).not.toContain("import { LayoutCoachMount }"); + // FAB stays scoped to the routed insights surface. + expect(source).toContain("LayoutCoachFab"); + }); +}); diff --git a/src/components/layout/auth-shell.tsx b/src/components/layout/auth-shell.tsx index b53dd0594..9732e7a7e 100644 --- a/src/components/layout/auth-shell.tsx +++ b/src/components/layout/auth-shell.tsx @@ -5,8 +5,10 @@ import { usePathname, useRouter } from "next/navigation"; import { useEffect } from "react"; import { AchievementUnlockNotifier } from "@/components/gamification/achievement-unlock-notifier"; import { MaintainershipBanner } from "@/components/i18n/maintainership-banner"; +import { LayoutCoachMount } from "@/components/insights/layout-coach-mount"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; +import { CoachLaunchProvider } from "@/lib/insights/coach-launch-context"; import { BottomNav } from "./bottom-nav"; import { SidebarNav } from "./sidebar-nav"; import { TopBar } from "./top-bar"; @@ -125,9 +127,19 @@ export function AuthShell({ children }: { children: React.ReactNode }) { ); } - // Authenticated — full app shell + // Authenticated — full app shell. + // + // v1.4.34 IW-B — the Coach launch provider lives here (not inside + // `app/insights/layout.tsx` anymore) so every authenticated route can + // call `askCoach()` from the same context. Pre-hoist the drawer was + // only reachable from `/insights/**`; the dashboard hero CTA now opens + // it without a route hop. `` consumes the same + // context to render the drawer once at the shell level — the + // insights-only mobile FAB stays where it is, inside the routed + // insights layout, so the bottom-right floating action sits beside + // the chart tooltips only on the surfaces that need it. return ( - <> + {showUnlockNotifier && user?.id ? ( ) : null} @@ -170,6 +182,7 @@ export function AuthShell({ children }: { children: React.ReactNode }) { - + + ); }