Files
HealthLog/src/lib/insights/coach-launch-context.tsx
T
Marc-André Bombeck c4ccd7e297 refactor(insights): migrate route slugs to english with redirects
Rename every routed Insights sub-page slug from German to English
(`/insights/blutdruck` -> `/insights/blood-pressure`, `gewicht` ->
`weight`, `medikamente` -> `medications`, and the full v1.7.0 metric
tail). UI copy stays localised through `t()` keys; only the internal
slugs change. The slug registry in `src/lib/insights/sub-page-metric.ts`
is the single source of truth, so the tab strip, target summary, and
daily-briefing routing targets follow it.

`next.config.ts` `redirects()` emits a permanent redirect from every
legacy German slug to its English target, including a `:path*` variant
so nested routes survive. Bookmarks and cached PWA navigation keep
resolving.
2026-05-31 22:12:25 +02:00

119 lines
4.0 KiB
TypeScript

"use client";
import {
createContext,
useCallback,
useContext,
useMemo,
useState,
type ReactNode,
} from "react";
import type { CoachScopeSource } from "@/lib/ai/coach/types";
/**
* v1.4.27 R3d MB4 — Coach launch context.
*
* Until v1.4.27 the Coach drawer was mounted inline on
* `/insights/page.tsx` body, which meant navigating to a routed
* sub-page (`/insights/blood-pressure`, etc.) unmounted the drawer and
* removed every entry point to the Coach. The MA3 audit + Decision F
* promoted the drawer up to `/insights/layout.tsx` so every routed
* Insights surface can launch it.
*
* Shape:
* - `<CoachLaunchProvider>` owns the drawer's open / prefill state
* and renders the children beneath the launch surface.
* - `useCoachLaunch()` returns `{ open, askCoach, setOpen, prefill }`
* so any descendant can open the drawer with an optional prefill
* string (the hero-strip suggested-prompt chips feed prefills;
* the sticky `<CoachLaunchButton>` opens without one).
*
* The provider is intentionally tiny — the actual drawer mount sits
* next to it in the layout, reading the same state via the hook.
*/
export interface CoachLaunchScope {
/**
* Optional metric the user is looking at when they open the Coach.
* Reserved for v1.4.28 so the drawer can pre-narrow the source rail
* to the active metric; ignored in v1.4.27.
*
* v1.4.28 R3c (BK-MED-2 / BK-F-M4) — narrowed from `string` to the
* `CoachScopeSource` union so call sites cannot drift to a free-form
* label that the sources rail then silently ignores. Adding a new
* Apple-Health source to the rail (the union already covers the
* v1.4.23 additions) automatically widens the allowed values here.
*/
metric?: CoachScopeSource;
}
interface CoachLaunchValue {
/** Whether the Coach drawer is currently open. */
open: boolean;
/** Current prefill string (or null when the next open should start blank). */
prefill: string | null;
/** Open the drawer with an optional prefill + scope hint. */
askCoach: (prefill?: string | null, scope?: CoachLaunchScope) => void;
/**
* Direct setter for the open state — the drawer's `onOpenChange`
* consumes it on close. Kept (rather than collapsed into a
* `closeCoach()` helper) because `<LayoutCoachMount>` forwards the
* raw setter to the Sheet's controlled-state contract, which expects
* a boolean callback. v1.4.28 R3c (BK-F-M4) audit: exactly one
* consumer, no drift.
*/
setOpen: (next: boolean) => void;
}
const CoachLaunchContext = createContext<CoachLaunchValue | null>(null);
export interface CoachLaunchProviderProps {
children: ReactNode;
}
export function CoachLaunchProvider({ children }: CoachLaunchProviderProps) {
const [open, setOpen] = useState<boolean>(false);
const [prefill, setPrefill] = useState<string | null>(null);
const askCoach = useCallback(
(nextPrefill?: string | null, scope?: CoachLaunchScope) => {
// `scope` is currently reserved for v1.4.28 (metric-narrow on the
// sources rail). Accept the parameter today so the call sites
// don't need to change shape when the rail starts honouring it.
void scope;
setPrefill(nextPrefill ?? null);
setOpen(true);
},
[],
);
const handleSetOpen = useCallback((next: boolean) => {
setOpen(next);
if (!next) {
// Drop the prefill on close so the next open starts blank.
setPrefill(null);
}
}, []);
const value = useMemo<CoachLaunchValue>(
() => ({ open, prefill, askCoach, setOpen: handleSetOpen }),
[open, prefill, askCoach, handleSetOpen],
);
return (
<CoachLaunchContext.Provider value={value}>
{children}
</CoachLaunchContext.Provider>
);
}
/**
* Read the Coach launch context. Returns `null` when called outside a
* `<CoachLaunchProvider>` so consumer components can degrade gracefully
* (e.g. the hero strip's "Ask the coach" action stays disabled until
* the provider mounts).
*/
export function useCoachLaunch(): CoachLaunchValue | null {
return useContext(CoachLaunchContext);
}