diff --git a/messages/de.json b/messages/de.json index 834c964cb..b80506688 100644 --- a/messages/de.json +++ b/messages/de.json @@ -890,8 +890,12 @@ "description": "Telegram, ntfy, Web Push." }, "dashboard": { - "title": "Übersicht", - "description": "Kachel-Layout, Schwellwerte." + "title": "Dashboard", + "description": "Kachel-Layout und Reihenfolge." + }, + "thresholds": { + "title": "Persönliche Zielwerte", + "description": "Eigene Zielbereiche für jede Metrik." }, "ai": { "title": "KI-Auswertungen", diff --git a/messages/en.json b/messages/en.json index 0b2704c5d..2171969f1 100644 --- a/messages/en.json +++ b/messages/en.json @@ -891,7 +891,11 @@ }, "dashboard": { "title": "Dashboard", - "description": "Tile layout, thresholds." + "description": "Tile layout and order." + }, + "thresholds": { + "title": "Personal targets", + "description": "Override target ranges for every metric." }, "ai": { "title": "AI Insights", diff --git a/src/app/page.tsx b/src/app/page.tsx index 456cc3936..3a85e8e18 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -460,66 +460,50 @@ export default function DashboardPage() { }); } if (showBpCards) { + // BP renders as ONE tile with paired sys/dia values (e.g. "117/79") + // so the strip width matches the chart strip below — and so the + // tile count stays at 5 instead of 6 with a wrap-around. The + // sparkline/trend arrow follows the systolic value (the common + // primary signal); the colour-banding tooltips cover sys only + // because dia uses a different target range and stuffing both + // into one hint became unreadable on mobile. trendCards.push({ id: "bp", order: widgetOrder("bp"), node: ( - - - - + ), }); } @@ -868,14 +852,14 @@ export default function DashboardPage() { * touch. */}
{trendCards.map((entry) => (
{entry.node}
diff --git a/src/app/settings/[section]/page.tsx b/src/app/settings/[section]/page.tsx index 31234c0b9..2e4ab29bd 100644 --- a/src/app/settings/[section]/page.tsx +++ b/src/app/settings/[section]/page.tsx @@ -10,6 +10,7 @@ import { DashboardSection } from "@/components/settings/dashboard-section"; import { IntegrationsSection } from "@/components/settings/integrations-section"; import { NotificationsSection } from "@/components/settings/notifications-section"; import { SectionPlaceholder } from "@/components/settings/section-placeholder"; +import { ThresholdsSettingsSection } from "@/components/settings/thresholds-settings-section"; import { SETTINGS_SECTION_SLUGS, isSettingsSectionSlug, @@ -42,6 +43,7 @@ const SECTION_COMPONENTS: Record< integrations: IntegrationsSection, notifications: NotificationsSection, dashboard: DashboardSection, + thresholds: ThresholdsSettingsSection, api: ApiSection, advanced: AdvancedSection, }; diff --git a/src/components/charts/trend-card.tsx b/src/components/charts/trend-card.tsx index 7ddbffce1..4ca248a9a 100644 --- a/src/components/charts/trend-card.tsx +++ b/src/components/charts/trend-card.tsx @@ -11,6 +11,13 @@ import { } from "@/components/ui/tooltip"; import { useTranslations, useFormatters } from "@/lib/i18n/context"; +interface SecondaryMetric { + /** Sub-value latest reading (e.g. diastolic when latest is systolic). */ + latest: number | null; + avg7: number | null; + avg30: number | null; +} + interface TrendCardProps { label: string; latest: number | null; @@ -23,6 +30,10 @@ interface TrendCardProps { avg30Hint?: React.ReactNode; slope30: TrendSlope | null; icon: React.ComponentType<{ className?: string }>; + /** Optional second value rendered next to the primary as `X / Y` (used for + * paired metrics like blood-pressure systolic/diastolic so a single tile + * shows both numbers). */ + secondary?: SecondaryMetric; } export function TrendCard({ @@ -37,6 +48,7 @@ export function TrendCard({ avg30Hint, slope30, icon: Icon, + secondary, }: TrendCardProps) { const { t } = useTranslations(); const fmt = useFormatters(); @@ -58,8 +70,19 @@ export function TrendCard({ const formatValue = (value: number) => fmt.number(value, 1); + const renderPair = ( + primary: number | null, + secondaryValue: number | null | undefined, + ): string => { + if (primary === null) return "—"; + if (secondary && secondaryValue !== null && secondaryValue !== undefined) { + return `${formatValue(primary)}/${formatValue(secondaryValue)}`; + } + return formatValue(primary); + }; + return ( -
+
{label} @@ -68,13 +91,15 @@ export function TrendCard({
- {latest !== null ? formatValue(latest) : "—"} + {latest !== null + ? renderPair(latest, secondary?.latest) + : "—"} {unit} {slope30 && }
-
+
{avg7 !== null && ( {t("charts.avg7dShort")}:{" "} @@ -82,7 +107,7 @@ export function TrendCard({ - {formatValue(avg7)} + {renderPair(avg7, secondary?.avg7)} @@ -91,7 +116,7 @@ export function TrendCard({ ) : ( - {formatValue(avg7)} + {renderPair(avg7, secondary?.avg7)} )} @@ -103,7 +128,7 @@ export function TrendCard({ - {formatValue(avg30)} + {renderPair(avg30, secondary?.avg30)} @@ -112,7 +137,7 @@ export function TrendCard({ ) : ( - {formatValue(avg30)} + {renderPair(avg30, secondary?.avg30)} )} diff --git a/src/components/settings/__tests__/sections.test.tsx b/src/components/settings/__tests__/sections.test.tsx index 7039fd19f..2eb944cc2 100644 --- a/src/components/settings/__tests__/sections.test.tsx +++ b/src/components/settings/__tests__/sections.test.tsx @@ -122,11 +122,16 @@ describe("settings sections — SSR smoke", () => { expect(html).toContain("Notifications"); }); - it(" renders heading and embeds layout + thresholds", () => { + it(" renders heading and embeds the layout customizer", () => { + // v1.4.3 split: the per-metric thresholds (Persönliche Zielwerte) moved + // out into their own settings section under `/settings/thresholds`, + // so the dashboard panel is now layout-only. Verifying the heading + // says "Dashboard" (not "Übersicht" — the v1.4.2 placeholder) and the + // layout slot still mounts. const html = render(); expect(html).toContain("Dashboard"); expect(html).toContain('data-testid="dashboard-layout"'); - expect(html).toContain('data-testid="thresholds"'); + expect(html).not.toContain('data-testid="thresholds"'); }); it(" renders endpoints + tokens cards", () => { diff --git a/src/components/settings/__tests__/settings-shell.test.tsx b/src/components/settings/__tests__/settings-shell.test.tsx index 06c1a9fcc..eb5d257f3 100644 --- a/src/components/settings/__tests__/settings-shell.test.tsx +++ b/src/components/settings/__tests__/settings-shell.test.tsx @@ -35,14 +35,18 @@ function renderShell(props: { } describe("SETTINGS_SECTION_SLUGS", () => { - it("declares exactly the eight sections agreed for v1.4", () => { + it("declares exactly the nine sections agreed for v1.4.3", () => { // Order matters — `generateStaticParams()` and the sidebar derive their // ordering from this constant, so a reorder is a behaviour change. + // v1.4.3 split the dashboard panel: layout stays under `dashboard`, + // per-metric overrides moved to their own `thresholds` slug so it's + // a top-level entry in the settings nav. expect([...SETTINGS_SECTION_SLUGS]).toEqual([ "account", "integrations", "notifications", "dashboard", + "thresholds", "ai", "api", "advanced", @@ -67,7 +71,7 @@ describe("SETTINGS_SECTION_SLUGS", () => { }); describe("", () => { - it("renders all eight section links — once for the mobile strip and once for the desktop sidebar", () => { + it("renders every section link — once for the mobile strip and once for the desktop sidebar", () => { const html = renderShell({ active: "account" }); for (const slug of SETTINGS_SECTION_SLUGS) { const matches = html.match(new RegExp(`href="/settings/${slug}"`, "g")); @@ -138,7 +142,12 @@ describe("", () => { expect(html).toContain("Konto"); expect(html).toContain("Integrationen"); expect(html).toContain("Benachrichtigungen"); - expect(html).toContain("Übersicht"); + // v1.4.3: the Settings sub-section formerly labelled "Übersicht" is now + // "Dashboard" (matching the term users see in the main nav). The + // per-metric overrides moved out into their own "Persönliche Zielwerte" + // section, which is the new entry below the Dashboard one. + expect(html).toContain("Dashboard"); + expect(html).toContain("Persönliche Zielwerte"); expect(html).toContain("KI-Auswertungen"); // API & Tokens is identical in both locales (proper noun + ampersand) expect(html).toContain("API & Tokens"); diff --git a/src/components/settings/dashboard-section.tsx b/src/components/settings/dashboard-section.tsx index 90a8b1728..6914aaaac 100644 --- a/src/components/settings/dashboard-section.tsx +++ b/src/components/settings/dashboard-section.tsx @@ -1,7 +1,6 @@ "use client"; import { DashboardLayoutSection } from "@/components/settings/dashboard-layout-section"; -import { ThresholdsSection } from "@/components/settings/thresholds-section"; import { useTranslations } from "@/lib/i18n/context"; export function DashboardSection() { @@ -25,7 +24,6 @@ export function DashboardSection() { - ); } diff --git a/src/components/settings/section-placeholder.tsx b/src/components/settings/section-placeholder.tsx index 5aff0d212..7e63de08c 100644 --- a/src/components/settings/section-placeholder.tsx +++ b/src/components/settings/section-placeholder.tsx @@ -18,6 +18,7 @@ import { LayoutDashboard, Link2, Settings2, + SlidersHorizontal, Sparkles, User, type LucideIcon, @@ -32,6 +33,7 @@ const SLUG_ICON: Record = { integrations: Link2, notifications: Bell, dashboard: LayoutDashboard, + thresholds: SlidersHorizontal, ai: Sparkles, api: KeyRound, advanced: Settings2, diff --git a/src/components/settings/section-slugs.ts b/src/components/settings/section-slugs.ts index 7198be4ea..5a69cbf0f 100644 --- a/src/components/settings/section-slugs.ts +++ b/src/components/settings/section-slugs.ts @@ -11,6 +11,7 @@ export const SETTINGS_SECTION_SLUGS = [ "integrations", "notifications", "dashboard", + "thresholds", "ai", "api", "advanced", diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index d329e3bbd..6434d543b 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -24,6 +24,7 @@ import { LayoutDashboard, Link2, Settings2, + SlidersHorizontal, Sparkles, User, type LucideIcon, @@ -79,6 +80,11 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [ titleKey: "settings.sections.dashboard.title", icon: LayoutDashboard, }, + { + slug: "thresholds", + titleKey: "settings.sections.thresholds.title", + icon: SlidersHorizontal, + }, { slug: "ai", titleKey: "settings.sections.ai.title", icon: Sparkles }, { slug: "api", titleKey: "settings.sections.api.title", icon: KeyRound }, { diff --git a/src/components/settings/thresholds-settings-section.tsx b/src/components/settings/thresholds-settings-section.tsx new file mode 100644 index 000000000..e55f745da --- /dev/null +++ b/src/components/settings/thresholds-settings-section.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { ThresholdsSection } from "@/components/settings/thresholds-section"; +import { useTranslations } from "@/lib/i18n/context"; + +export function ThresholdsSettingsSection() { + const { t } = useTranslations(); + + return ( +
+
+

+ {t("settings.sections.thresholds.title")} +

+

+ {t("settings.sections.thresholds.description")} +

+
+ + +
+ ); +}