mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
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:
1 parent
001c158203
commit
78c110a6c8
7 files changed
+266
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user