feat(settings): add a Reminders & Notifications home

Reminders used to be scattered across three places: medication reminders on
each med card, mood / low-stock / coach-nudge inside the Notifications channel
screen, and the Vorsorge reminders on their own page. The new Reminders
section is the single front door. It draws a clean line between the two
concepts — reminder categories (the what/when) and notification channels (the
how/where) — and links to each canonical editor, which keeps its own route and
deep link intact. Consolidation by linking, not a rewrite.

The section sits right after Notifications in the settings nav.
This commit is contained in:
Marc-André Bombeck committed 2026-06-15 00:59:47 +02:00
1 parent 001c158203
commit 78c110a6c8
7 files changed
+266

No files matched your search

+2
View File
@@ -14,6 +14,7 @@ import { LayoutSection } from "@/components/settings/layout-section";
import { MedicationsSection } from "@/components/settings/medications-section";
import { MoodSection } from "@/components/settings/mood-section";
import { NotificationsSection } from "@/components/settings/notifications-section";
import { RemindersSection } from "@/components/settings/reminders-section";
import { SectionPlaceholder } from "@/components/settings/section-placeholder";
// v1.8.7.1 — `thresholds` (Targets) and `sources` (Sources) are two
// separate sections again. `ThresholdsSection` renders the target-range
@@ -52,6 +53,7 @@ const SECTION_COMPONENTS: Record<
ai: AiSection,
integrations: IntegrationsSection,
notifications: NotificationsSection,
reminders: RemindersSection,
layout: LayoutSection,
dashboard: DashboardSection,
insights: InsightsSection,
@@ -0,0 +1,67 @@
/**
* v1.17.1 — the "Reminders & Notifications" hub.
*
* The hub is the single front door for every reminder concept. It groups the
* reminder CATEGORIES (medication / Vorsorge / mood / low-stock / coach) and
* links to the notification CHANNELS, with each link deep-linking into the
* canonical editor so nothing is rewritten — only gathered in one place.
*/
import { describe, expect, it } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
import { I18nProvider } from "@/lib/i18n/context";
import { RemindersSection } from "../reminders-section";
function render(locale: "en" | "de" = "en") {
return renderToStaticMarkup(
<I18nProvider initialLocale={locale}>
<RemindersSection />
</I18nProvider>,
);
}
describe("<RemindersSection>", () => {
it("deep-links to every canonical reminder editor", () => {
const html = render();
for (const href of [
"/medications",
"/vorsorge",
"/settings/notifications#mood-reminder",
"/settings/notifications#low-stock",
"/settings/notifications#coach-nudge",
]) {
expect(html).toContain(`href="${href}"`);
}
});
it("links to the notification channels screen (the how/where)", () => {
const html = render();
expect(html).toContain('href="/settings/notifications"');
expect(html).toContain('data-testid="reminders-link-channels"');
});
it("renders a card for each reminder category", () => {
const html = render();
for (const testId of [
"reminders-link-medication",
"reminders-link-vorsorge",
"reminders-link-mood",
"reminders-link-low-stock",
"reminders-link-coach",
]) {
expect(html).toContain(`data-testid="${testId}"`);
}
});
it("separates categories (what/when) from channels (how/where)", () => {
const html = render();
expect(html).toContain("Reminders");
expect(html).toContain("Delivery channels");
});
it("resolves its copy in German too", () => {
const html = render("de");
expect(html).toContain("Medikamenten-Erinnerungen");
expect(html).toContain("Zustellkanäle");
});
});
@@ -61,10 +61,13 @@ describe("SETTINGS_SECTION_SLUGS", () => {
// slugs (`dashboard`, `insights`, `medications`, `mood`) keep their
// routes so deep links resolve, but they are reached through the
// Layout hub instead of four standalone nav entries.
// v1.17.1 — `reminders` is the one "Reminders & Notifications" home and
// sits right after `notifications`.
expect([...SETTINGS_SECTION_SLUGS]).toEqual([
"account",
"integrations",
"notifications",
"reminders",
"layout",
"dashboard",
"insights",
@@ -0,0 +1,176 @@
"use client";
import Link from "next/link";
import {
Bell,
ChevronRight,
HeartPulse,
MessageCircle,
Package,
Pill,
Smile,
Stethoscope,
type LucideIcon,
} from "lucide-react";
import { cn } from "@/lib/utils";
import { useTranslations } from "@/lib/i18n/context";
/**
* v1.17.1 — the one "Reminders & Notifications" home.
*
* Before this section, "what gets reminded and when" was scattered across
* three unrelated homes: medication reminders lived per-medication on the med
* cards, mood + low-stock + coach-nudge lived inside the Notifications channel
* screen, and the new Vorsorge (preventive-care) reminders had their own
* page. There was no single place that answered "was erinnert mich woran,
* wann, und worüber".
*
* This hub is the single front door. It draws a clean line between two
* genuinely different concepts (so the maintainer rule "never split the same
* concept across a page" still holds):
*
* - CHANNELS — the *how/where* a reminder is delivered (APNs / Telegram /
* ntfy / Web Push / Webhook / Email). These keep living on the
* Notifications channel screen; this hub links to them.
* - CATEGORIES — the *what/when*: medication, mood, Vorsorge, low-stock
* runway, and the proactive Coach nudge. Each keeps its own canonical
* editor and deep link; the hub just gathers them so the concept reads
* as one place.
*
* Consolidation by linking — every existing editor stays intact and
* deep-linkable; nothing here is a rewrite.
*/
interface ReminderLink {
href: string;
icon: LucideIcon;
titleKey: string;
descriptionKey: string;
/** External-to-settings link (opens a feature page, not a settings tab). */
testId: string;
}
const CHANNEL_LINKS: ReadonlyArray<ReminderLink> = [
{
href: "/settings/notifications",
icon: Bell,
titleKey: "settings.sections.reminders.channels.title",
descriptionKey: "settings.sections.reminders.channels.description",
testId: "reminders-link-channels",
},
];
const CATEGORY_LINKS: ReadonlyArray<ReminderLink> = [
{
href: "/medications",
icon: Pill,
titleKey: "settings.sections.reminders.medication.title",
descriptionKey: "settings.sections.reminders.medication.description",
testId: "reminders-link-medication",
},
{
href: "/vorsorge",
icon: Stethoscope,
titleKey: "settings.sections.reminders.vorsorge.title",
descriptionKey: "settings.sections.reminders.vorsorge.description",
testId: "reminders-link-vorsorge",
},
{
href: "/settings/notifications#mood-reminder",
icon: Smile,
titleKey: "settings.sections.reminders.mood.title",
descriptionKey: "settings.sections.reminders.mood.description",
testId: "reminders-link-mood",
},
{
href: "/settings/notifications#low-stock",
icon: Package,
titleKey: "settings.sections.reminders.lowStock.title",
descriptionKey: "settings.sections.reminders.lowStock.description",
testId: "reminders-link-low-stock",
},
{
href: "/settings/notifications#coach-nudge",
icon: MessageCircle,
titleKey: "settings.sections.reminders.coach.title",
descriptionKey: "settings.sections.reminders.coach.description",
testId: "reminders-link-coach",
},
];
function ReminderLinkCard({ link }: { link: ReminderLink }) {
const { t } = useTranslations();
const Icon = link.icon;
return (
<li>
<Link
href={link.href}
data-testid={link.testId}
className={cn(
"bg-card border-border hover:bg-accent/40 group flex items-center gap-4 rounded-xl border p-4 transition-colors sm:p-5",
)}
>
<Icon
className="text-muted-foreground h-5 w-5 shrink-0"
aria-hidden="true"
/>
<div className="min-w-0 flex-1 space-y-0.5">
<p className="text-sm font-semibold">{t(link.titleKey)}</p>
<p className="text-muted-foreground text-xs">
{t(link.descriptionKey)}
</p>
</div>
<ChevronRight
className="text-muted-foreground/60 group-hover:text-foreground h-4 w-4 shrink-0 transition-colors"
aria-hidden="true"
/>
</Link>
</li>
);
}
export function RemindersSection() {
const { t } = useTranslations();
return (
<section
aria-labelledby="settings-section-reminders-title"
className="space-y-6"
>
<header className="space-y-1">
<h1 id="settings-section-reminders-title" className="sr-only">
{t("settings.sections.reminders.title")}
</h1>
<p className="text-muted-foreground text-sm">
{t("settings.sections.reminders.description")}
</p>
</header>
{/* CATEGORIES — the what/when. */}
<div className="space-y-3">
<div className="text-muted-foreground flex items-center gap-2 text-xs font-medium tracking-wide uppercase">
<HeartPulse className="h-3.5 w-3.5" aria-hidden="true" />
{t("settings.sections.reminders.categoriesGroup")}
</div>
<ul className="space-y-3">
{CATEGORY_LINKS.map((link) => (
<ReminderLinkCard key={link.href} link={link} />
))}
</ul>
</div>
{/* CHANNELS — the how/where. */}
<div className="space-y-3">
<div className="text-muted-foreground flex items-center gap-2 text-xs font-medium tracking-wide uppercase">
<Bell className="h-3.5 w-3.5" aria-hidden="true" />
{t("settings.sections.reminders.channelsGroup")}
</div>
<ul className="space-y-3">
{CHANNEL_LINKS.map((link) => (
<ReminderLinkCard key={link.href} link={link} />
))}
</ul>
</div>
</section>
);
}
@@ -12,6 +12,7 @@
*/
import {
AlarmClock,
Bell,
Download,
Info,
@@ -37,6 +38,7 @@ const SLUG_ICON: Record<SettingsSectionSlug, LucideIcon> = {
account: User,
integrations: Link2,
notifications: Bell,
reminders: AlarmClock,
layout: LayoutDashboard,
dashboard: LayoutDashboard,
insights: TrendingUp,
+6
View File
@@ -27,10 +27,16 @@
// `layout` entry instead of four scattered "arrange" entries, so the
// concept reads as one place. The four editor slugs stay in this list so
// their routes still resolve; they are simply not listed in the shell nav.
// v1.17.1 — `reminders` sits right after `notifications`: the one
// "Reminders & Notifications" home that gathers the scattered reminder
// categories (medication, mood, Vorsorge, low-stock, coach nudge) and links
// to the notification channels. The canonical editors keep their own routes;
// the hub is consolidation by linking.
export const SETTINGS_SECTION_SLUGS = [
"account",
"integrations",
"notifications",
"reminders",
"layout",
"dashboard",
"insights",
@@ -18,6 +18,7 @@ import * as React from "react";
import Link from "next/link";
import { usePathname } from "next/navigation";
import {
AlarmClock,
Bell,
Download,
Info,
@@ -89,6 +90,15 @@ export const SETTINGS_SECTIONS: readonly SettingsSection[] = [
titleKey: "settings.sections.notifications.title",
icon: Bell,
},
// v1.17.1 — the one "Reminders & Notifications" home. Gathers the
// scattered reminder categories (medication / mood / Vorsorge / low-stock
// / coach nudge) and links to the notification channels. The canonical
// editors keep their own routes; this nav entry is the single front door.
{
slug: "reminders",
titleKey: "settings.sections.reminders.title",
icon: AlarmClock,
},
// v1.17.1 (F-2) — one "Layout & Personalization" nav entry replaces the
// four scattered Dashboard / Insights / Medications / Mood "arrange"
// entries. Those routes still resolve (deep links, page-header cogs, and