mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 21:32:18 +00:00
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.
This commit is contained in:
1 parent
b416cabbf1
commit
c4ccd7e297
46 files changed
+244
-180
No files matched your search
@@ -80,7 +80,7 @@ const BP_DATA: TargetsFixture = {
|
||||
|
||||
describe("<MetricTargetSummary>", () => {
|
||||
it("renders the numeric range + in-target share for a simple metric", () => {
|
||||
const html = renderWith("gewicht", WEIGHT_DATA);
|
||||
const html = renderWith("weight", WEIGHT_DATA);
|
||||
// Trailing `.0` trimmed, single-decimal kept.
|
||||
expect(html).toContain("Target: 60.1–80.9 kg");
|
||||
// round(18/24*100) = 75
|
||||
@@ -88,19 +88,19 @@ describe("<MetricTargetSummary>", () => {
|
||||
});
|
||||
|
||||
it("stitches the systolic + diastolic bands for blood pressure", () => {
|
||||
const html = renderWith("blutdruck", BP_DATA);
|
||||
const html = renderWith("blood-pressure", BP_DATA);
|
||||
expect(html).toContain("Target: 120–129 / 70–79 mmHg");
|
||||
expect(html).toContain("50% of logged days within target");
|
||||
});
|
||||
|
||||
it("renders the adjust-target link to /targets", () => {
|
||||
const html = renderWith("gewicht", WEIGHT_DATA);
|
||||
const html = renderWith("weight", WEIGHT_DATA);
|
||||
expect(html).toContain('href="/targets"');
|
||||
expect(html).toContain("Adjust target range");
|
||||
});
|
||||
|
||||
it("suppresses the share when the route flags insufficient data", () => {
|
||||
const html = renderWith("gewicht", {
|
||||
const html = renderWith("weight", {
|
||||
...WEIGHT_DATA,
|
||||
targets: [{ ...WEIGHT_DATA.targets[0], insufficientData: true }],
|
||||
});
|
||||
@@ -114,12 +114,12 @@ describe("<MetricTargetSummary>", () => {
|
||||
});
|
||||
|
||||
it("renders nothing before the payload resolves", () => {
|
||||
const html = renderWith("gewicht", undefined);
|
||||
const html = renderWith("weight", undefined);
|
||||
expect(html).toBe("");
|
||||
});
|
||||
|
||||
it("renders nothing when the matching target has no range", () => {
|
||||
const html = renderWith("gewicht", {
|
||||
const html = renderWith("weight", {
|
||||
...WEIGHT_DATA,
|
||||
targets: [{ ...WEIGHT_DATA.targets[0], range: null }],
|
||||
});
|
||||
|
||||
@@ -39,23 +39,23 @@ import type {
|
||||
* through to `null` and render as a plain row (no link wrap).
|
||||
*/
|
||||
const METRIC_HREF: Record<DailyBriefingKeyFinding["sourceMetric"], string | null> = {
|
||||
bp: "/insights/blutdruck",
|
||||
weight: "/insights/gewicht",
|
||||
pulse: "/insights/puls",
|
||||
mood: "/insights/stimmung",
|
||||
compliance: "/insights/medikamente",
|
||||
bp: "/insights/blood-pressure",
|
||||
weight: "/insights/weight",
|
||||
pulse: "/insights/pulse",
|
||||
mood: "/insights/mood",
|
||||
compliance: "/insights/medications",
|
||||
// Apple Health / GLP-1 additive metrics — no dedicated sub-page yet.
|
||||
// They render as static rows; v1.4.28 can wire each as they ship.
|
||||
hrv: null,
|
||||
sleep: "/insights/schlaf",
|
||||
sleep: "/insights/sleep",
|
||||
resting_hr: null,
|
||||
steps: null,
|
||||
active_energy: null,
|
||||
flights: null,
|
||||
distance: null,
|
||||
vo2_max: "/insights/puls",
|
||||
vo2_max: "/insights/pulse",
|
||||
body_temp: null,
|
||||
glp1_plateau: "/insights/medikamente",
|
||||
glp1_plateau: "/insights/medications",
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,7 +31,7 @@ import {
|
||||
* v1.4.25 W4 — routed tab strip for `/insights`.
|
||||
*
|
||||
* Behaviour evolved from the v1.4.25 W3 scroll-anchor version: each pill
|
||||
* is now a `<Link>` to a routed sub-page (`/insights/blutdruck` …),
|
||||
* is now a `<Link>` to a routed sub-page (`/insights/blood-pressure` …),
|
||||
* `usePathname()` decides the active pill, and the strip is mounted in
|
||||
* `src/app/insights/layout.tsx` so it persists across navigation
|
||||
* without re-rendering. The CoachDrawer is NOT mounted in this layout —
|
||||
@@ -116,47 +116,47 @@ const SUB_PAGE_TABS: Record<
|
||||
{ labelKey: string; metric: InsightMetric }
|
||||
> = {
|
||||
// ── vitals ──
|
||||
blutdruck: {
|
||||
"blood-pressure": {
|
||||
labelKey: "insights.navBloodPressure",
|
||||
metric: "BLOOD_PRESSURE_SYS",
|
||||
},
|
||||
puls: { labelKey: "insights.navPulse", metric: "PULSE" },
|
||||
sauerstoff: {
|
||||
pulse: { labelKey: "insights.navPulse", metric: "PULSE" },
|
||||
oxygen: {
|
||||
labelKey: "insights.navOxygenSaturation",
|
||||
metric: "OXYGEN_SATURATION",
|
||||
},
|
||||
koerpertemperatur: {
|
||||
"body-temperature": {
|
||||
labelKey: "insights.navBodyTemperature",
|
||||
metric: "BODY_TEMPERATURE",
|
||||
},
|
||||
atemfrequenz: {
|
||||
"respiratory-rate": {
|
||||
labelKey: "insights.navRespiratoryRate",
|
||||
metric: "RESPIRATORY_RATE",
|
||||
},
|
||||
// ── body composition ──
|
||||
gewicht: { labelKey: "insights.navWeight", metric: "WEIGHT" },
|
||||
weight: { labelKey: "insights.navWeight", metric: "WEIGHT" },
|
||||
bmi: { labelKey: "insights.navBmi", metric: "BMI" },
|
||||
koerperwasser: {
|
||||
"body-water": {
|
||||
labelKey: "insights.navTotalBodyWater",
|
||||
metric: "TOTAL_BODY_WATER",
|
||||
},
|
||||
knochenmasse: { labelKey: "insights.navBoneMass", metric: "BONE_MASS" },
|
||||
"fettfreie-masse": {
|
||||
"bone-mass": { labelKey: "insights.navBoneMass", metric: "BONE_MASS" },
|
||||
"fat-free-mass": {
|
||||
labelKey: "insights.navFatFreeMass",
|
||||
metric: "FAT_FREE_MASS",
|
||||
},
|
||||
fettmasse: { labelKey: "insights.navFatMass", metric: "FAT_MASS" },
|
||||
muskelmasse: { labelKey: "insights.navMuscleMass", metric: "MUSCLE_MASS" },
|
||||
viszeralfett: {
|
||||
"fat-mass": { labelKey: "insights.navFatMass", metric: "FAT_MASS" },
|
||||
"muscle-mass": { labelKey: "insights.navMuscleMass", metric: "MUSCLE_MASS" },
|
||||
"visceral-fat": {
|
||||
labelKey: "insights.navVisceralFat",
|
||||
metric: "VISCERAL_FAT",
|
||||
},
|
||||
magermasse: {
|
||||
"lean-body-mass": {
|
||||
labelKey: "insights.navLeanBodyMass",
|
||||
metric: "LEAN_BODY_MASS",
|
||||
},
|
||||
// ── activity ──
|
||||
"aktive-energie": {
|
||||
"active-energy": {
|
||||
labelKey: "insights.navActiveEnergy",
|
||||
metric: "ACTIVE_ENERGY_BURNED",
|
||||
},
|
||||
@@ -164,85 +164,85 @@ const SUB_PAGE_TABS: Record<
|
||||
// availability helper reads `inputs.hasWorkouts` rather than a
|
||||
// `summaries[…].count`.
|
||||
workouts: { labelKey: "insights.navWorkouts", metric: "WORKOUTS" },
|
||||
stockwerke: {
|
||||
"flights-climbed": {
|
||||
labelKey: "insights.navFlightsClimbed",
|
||||
metric: "FLIGHTS_CLIMBED",
|
||||
},
|
||||
gehstrecke: {
|
||||
"walking-distance": {
|
||||
labelKey: "insights.navWalkingRunningDistance",
|
||||
metric: "WALKING_RUNNING_DISTANCE",
|
||||
},
|
||||
gangstabilitaet: {
|
||||
"walking-steadiness": {
|
||||
labelKey: "insights.navWalkingSteadiness",
|
||||
metric: "WALKING_STEADINESS",
|
||||
},
|
||||
gehpuls: {
|
||||
"walking-heart-rate": {
|
||||
labelKey: "insights.navWalkingHeartRateAverage",
|
||||
metric: "WALKING_HEART_RATE_AVERAGE",
|
||||
},
|
||||
gangasymmetrie: {
|
||||
"walking-asymmetry": {
|
||||
labelKey: "insights.navWalkingAsymmetry",
|
||||
metric: "WALKING_ASYMMETRY",
|
||||
},
|
||||
doppelstandphase: {
|
||||
"double-support-time": {
|
||||
labelKey: "insights.navWalkingDoubleSupport",
|
||||
metric: "WALKING_DOUBLE_SUPPORT",
|
||||
},
|
||||
schrittlaenge: {
|
||||
"step-length": {
|
||||
labelKey: "insights.navWalkingStepLength",
|
||||
metric: "WALKING_STEP_LENGTH",
|
||||
},
|
||||
gehgeschwindigkeit: {
|
||||
"walking-speed": {
|
||||
labelKey: "insights.navWalkingSpeed",
|
||||
metric: "WALKING_SPEED",
|
||||
},
|
||||
// ── sleep ──
|
||||
schlaf: { labelKey: "insights.navSleep", metric: "SLEEP_DURATION" },
|
||||
sleep: { labelKey: "insights.navSleep", metric: "SLEEP_DURATION" },
|
||||
// ── cardiovascular ──
|
||||
ruhepuls: {
|
||||
"resting-pulse": {
|
||||
labelKey: "insights.navRestingHr",
|
||||
metric: "RESTING_HEART_RATE",
|
||||
},
|
||||
hrv: { labelKey: "insights.navHrv", metric: "HEART_RATE_VARIABILITY" },
|
||||
pulswellengeschwindigkeit: {
|
||||
"pulse-wave-velocity": {
|
||||
labelKey: "insights.navPulseWaveVelocity",
|
||||
metric: "PULSE_WAVE_VELOCITY",
|
||||
},
|
||||
gefaessalter: {
|
||||
"vascular-age": {
|
||||
labelKey: "insights.navVascularAge",
|
||||
metric: "VASCULAR_AGE",
|
||||
},
|
||||
// ── hearing ──
|
||||
laermbelastung: {
|
||||
"environmental-audio": {
|
||||
labelKey: "insights.navAudioExposureEnv",
|
||||
metric: "AUDIO_EXPOSURE_ENV",
|
||||
},
|
||||
kopfhoererpegel: {
|
||||
"headphone-audio": {
|
||||
labelKey: "insights.navAudioExposureHeadphone",
|
||||
metric: "AUDIO_EXPOSURE_HEADPHONE",
|
||||
},
|
||||
laermereignisse: {
|
||||
"audio-events": {
|
||||
labelKey: "insights.navAudioExposureEvent",
|
||||
metric: "AUDIO_EXPOSURE_EVENT",
|
||||
},
|
||||
// ── environment ──
|
||||
tageslicht: {
|
||||
daylight: {
|
||||
labelKey: "insights.navTimeInDaylight",
|
||||
metric: "TIME_IN_DAYLIGHT",
|
||||
},
|
||||
// ── metabolic ──
|
||||
blutzucker: {
|
||||
"blood-glucose": {
|
||||
labelKey: "insights.navBloodGlucose",
|
||||
metric: "BLOOD_GLUCOSE",
|
||||
},
|
||||
hauttemperatur: {
|
||||
"skin-temperature": {
|
||||
labelKey: "insights.navSkinTemperature",
|
||||
metric: "SKIN_TEMPERATURE",
|
||||
},
|
||||
// ── mood ──
|
||||
stimmung: { labelKey: "insights.navMood", metric: "MOOD" },
|
||||
mood: { labelKey: "insights.navMood", metric: "MOOD" },
|
||||
// ── events ──
|
||||
medikamente: { labelKey: "insights.navMedication", metric: "MEDICATION" },
|
||||
medications: { labelKey: "insights.navMedication", metric: "MEDICATION" },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -409,7 +409,7 @@ function InsightsTabStripImpl({
|
||||
if (tab.kind === "link") {
|
||||
// The overview pill matches the mother page exactly; the
|
||||
// sub-page pills match a prefix so future nested routes
|
||||
// (e.g. `/insights/schlaf/2026-05-11`) still highlight the
|
||||
// (e.g. `/insights/sleep/2026-05-11`) still highlight the
|
||||
// parent tab.
|
||||
const isActive =
|
||||
tab.href === INSIGHTS_OVERVIEW_PATH
|
||||
|
||||
@@ -67,17 +67,17 @@ interface TargetsResponse {
|
||||
* component renders nothing for them, so the sub-page collapses cleanly.
|
||||
*/
|
||||
const SLUG_TO_TARGET_TYPE: Record<string, string> = {
|
||||
blutdruck: "BLOOD_PRESSURE",
|
||||
gewicht: "WEIGHT",
|
||||
"blood-pressure": "BLOOD_PRESSURE",
|
||||
weight: "WEIGHT",
|
||||
bmi: "BMI",
|
||||
puls: "PULSE",
|
||||
schlaf: "SLEEP_DURATION",
|
||||
stimmung: "MOOD_SCORE",
|
||||
medikamente: "MEDICATION_COMPLIANCE",
|
||||
pulse: "PULSE",
|
||||
sleep: "SLEEP_DURATION",
|
||||
mood: "MOOD_SCORE",
|
||||
medications: "MEDICATION_COMPLIANCE",
|
||||
};
|
||||
|
||||
interface MetricTargetSummaryProps {
|
||||
/** Insights category slug, e.g. `"blutdruck"`. */
|
||||
/** Insights category slug, e.g. `"blood-pressure"`. */
|
||||
slug: string;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
} from "@/lib/medications/injection-sites";
|
||||
|
||||
/**
|
||||
* v1.4.25 W4d — therapy-timeline view for /insights/medikamente.
|
||||
* v1.4.25 W4d — therapy-timeline view for /insights/medications.
|
||||
*
|
||||
* Renders a chronological list of GLP-1 therapy events: dose changes,
|
||||
* injections (with site if recorded), inventory events, side-effect tag
|
||||
|
||||
@@ -9,7 +9,7 @@ import { cn } from "@/lib/utils";
|
||||
import { HealthChartDynamic } from "@/components/charts/health-chart-dynamic";
|
||||
|
||||
/**
|
||||
* v1.4.25 W16a — VO2 max chart-row card for `/insights/puls`.
|
||||
* v1.4.25 W16a — VO2 max chart-row card for `/insights/pulse`.
|
||||
*
|
||||
* The dashboard already carries an opt-in VO2 max trend tile (W8d.5);
|
||||
* this is the deeper Insights surface. Renders one HealthChart for the
|
||||
|
||||
Reference in new issue
Block a user