mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 13:01:00 +00:00
merge(settings): reminders hub, uniform integration cards, per-integration setup-guide links
This commit is contained in:
commit
a27247f93c
20 files changed
+569
-6
No files matched your search
+32
-1
@@ -4087,6 +4087,36 @@
|
||||
"mood": {
|
||||
"title": "Stimmungs-Tags",
|
||||
"description": "Gruppen, Tags und Reihenfolge der Tag-Auswahl."
|
||||
},
|
||||
"reminders": {
|
||||
"title": "Erinnerungen & Benachrichtigungen",
|
||||
"description": "Ein Ort für jede Erinnerung — woran und wann erinnert wird — und die Kanäle, die sie zustellen.",
|
||||
"categoriesGroup": "Erinnerungen",
|
||||
"channelsGroup": "Zustellkanäle",
|
||||
"medication": {
|
||||
"title": "Medikamenten-Erinnerungen",
|
||||
"description": "Einnahmezeiten und Zeitfenster, je Medikament eingestellt."
|
||||
},
|
||||
"vorsorge": {
|
||||
"title": "Vorsorge",
|
||||
"description": "Wiederkehrende Untersuchungen und Messungen, inklusive Ort."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Stimmungs-Check-in",
|
||||
"description": "Eine tägliche Erinnerung, dein Befinden zu erfassen."
|
||||
},
|
||||
"lowStock": {
|
||||
"title": "Reichweite bei Restbestand",
|
||||
"description": "Werde gewarnt, bevor ein Medikament zur Neige geht."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Coach-Hinweis",
|
||||
"description": "Proaktive Impulse vom KI-Coach."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Benachrichtigungskanäle",
|
||||
"description": "APNs, Telegram, ntfy, Web Push, Webhook und E-Mail."
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": "Profil",
|
||||
@@ -4773,7 +4803,8 @@
|
||||
"token": "Der Token-Austausch mit Oura ist fehlgeschlagen. Bitte starte die Verbindung erneut.",
|
||||
"rate_limited": "Zu viele Verbindungsversuche. Warte einen Moment und versuche es erneut.",
|
||||
"generic": "Unbekannter Fehler bei der Verbindung. Bitte starte die Verbindung erneut."
|
||||
}
|
||||
},
|
||||
"integrationSetupGuide": "Einrichtungsanleitung"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+32
-1
@@ -4087,6 +4087,36 @@
|
||||
"mood": {
|
||||
"title": "Mood tags",
|
||||
"description": "Groups, tags, and order of the mood picker."
|
||||
},
|
||||
"reminders": {
|
||||
"title": "Reminders & Notifications",
|
||||
"description": "One home for every reminder — what gets reminded and when — plus the channels that deliver them.",
|
||||
"categoriesGroup": "Reminders",
|
||||
"channelsGroup": "Delivery channels",
|
||||
"medication": {
|
||||
"title": "Medication reminders",
|
||||
"description": "Dose times and windows, configured on each medication."
|
||||
},
|
||||
"vorsorge": {
|
||||
"title": "Preventive care",
|
||||
"description": "Recurring check-ups and measurements, with where to go."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Mood check-in",
|
||||
"description": "A daily nudge to log how you feel."
|
||||
},
|
||||
"lowStock": {
|
||||
"title": "Low-stock runway",
|
||||
"description": "Get warned before a medication runs out."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Coach nudge",
|
||||
"description": "Proactive prompts from the AI coach."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Notification channels",
|
||||
"description": "APNs, Telegram, ntfy, Web Push, Webhook, and Email."
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": "Profile",
|
||||
@@ -4773,7 +4803,8 @@
|
||||
"token": "The token exchange with Oura failed. Please start the connection again.",
|
||||
"rate_limited": "Too many connection attempts. Wait a moment and try again.",
|
||||
"generic": "Unknown error while connecting. Please start the connection again."
|
||||
}
|
||||
},
|
||||
"integrationSetupGuide": "Setup guide"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+32
-1
@@ -4087,6 +4087,36 @@
|
||||
"mood": {
|
||||
"title": "Etiquetas de ánimo",
|
||||
"description": "Grupos, etiquetas y orden del selector de ánimo."
|
||||
},
|
||||
"reminders": {
|
||||
"title": "Recordatorios y notificaciones",
|
||||
"description": "Un único lugar para cada recordatorio — qué se recuerda y cuándo — y los canales que lo entregan.",
|
||||
"categoriesGroup": "Recordatorios",
|
||||
"channelsGroup": "Canales de entrega",
|
||||
"medication": {
|
||||
"title": "Recordatorios de medicación",
|
||||
"description": "Horas y ventanas de dosis, configuradas en cada medicamento."
|
||||
},
|
||||
"vorsorge": {
|
||||
"title": "Cuidado preventivo",
|
||||
"description": "Revisiones y mediciones periódicas, con el lugar al que acudir."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Registro de ánimo",
|
||||
"description": "Un aviso diario para registrar cómo te sientes."
|
||||
},
|
||||
"lowStock": {
|
||||
"title": "Margen por bajo stock",
|
||||
"description": "Recibe un aviso antes de que se acabe un medicamento."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Sugerencia del coach",
|
||||
"description": "Indicaciones proactivas del coach de IA."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Canales de notificación",
|
||||
"description": "APNs, Telegram, ntfy, Web Push, Webhook y correo."
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": "Perfil",
|
||||
@@ -4773,7 +4803,8 @@
|
||||
"token": "El intercambio de token con Oura falló. Inicia la conexión de nuevo.",
|
||||
"rate_limited": "Demasiados intentos de conexión. Espera un momento e inténtalo de nuevo.",
|
||||
"generic": "Error desconocido al conectar. Inicia la conexión de nuevo."
|
||||
}
|
||||
},
|
||||
"integrationSetupGuide": "Guía de configuración"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+32
-1
@@ -4087,6 +4087,36 @@
|
||||
"mood": {
|
||||
"title": "Étiquettes d’humeur",
|
||||
"description": "Groupes, étiquettes et ordre du sélecteur d’humeur."
|
||||
},
|
||||
"reminders": {
|
||||
"title": "Rappels et notifications",
|
||||
"description": "Un seul endroit pour chaque rappel — quoi et quand — ainsi que les canaux qui les délivrent.",
|
||||
"categoriesGroup": "Rappels",
|
||||
"channelsGroup": "Canaux de diffusion",
|
||||
"medication": {
|
||||
"title": "Rappels de médication",
|
||||
"description": "Heures et fenêtres de prise, configurées sur chaque médicament."
|
||||
},
|
||||
"vorsorge": {
|
||||
"title": "Prévention",
|
||||
"description": "Bilans et mesures récurrents, avec le lieu où se rendre."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Suivi de l'humeur",
|
||||
"description": "Un rappel quotidien pour noter votre ressenti."
|
||||
},
|
||||
"lowStock": {
|
||||
"title": "Marge de stock faible",
|
||||
"description": "Soyez averti avant qu'un médicament ne s'épuise."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Suggestion du coach",
|
||||
"description": "Invitations proactives du coach IA."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Canaux de notification",
|
||||
"description": "APNs, Telegram, ntfy, Web Push, Webhook et e-mail."
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": "Profil",
|
||||
@@ -4773,7 +4803,8 @@
|
||||
"token": "L'échange de jeton avec Oura a échoué. Veuillez relancer la connexion.",
|
||||
"rate_limited": "Trop de tentatives de connexion. Patientez un instant et réessayez.",
|
||||
"generic": "Erreur inconnue lors de la connexion. Veuillez relancer la connexion."
|
||||
}
|
||||
},
|
||||
"integrationSetupGuide": "Guide de configuration"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+32
-1
@@ -4087,6 +4087,36 @@
|
||||
"mood": {
|
||||
"title": "Tag dell’umore",
|
||||
"description": "Gruppi, tag e ordine del selettore dell’umore."
|
||||
},
|
||||
"reminders": {
|
||||
"title": "Promemoria e notifiche",
|
||||
"description": "Un'unica sede per ogni promemoria — cosa ricordare e quando — e i canali che li recapitano.",
|
||||
"categoriesGroup": "Promemoria",
|
||||
"channelsGroup": "Canali di consegna",
|
||||
"medication": {
|
||||
"title": "Promemoria dei farmaci",
|
||||
"description": "Orari e finestre delle dosi, impostati su ogni farmaco."
|
||||
},
|
||||
"vorsorge": {
|
||||
"title": "Prevenzione",
|
||||
"description": "Controlli e misurazioni ricorrenti, con il luogo dove recarsi."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Check-in dell'umore",
|
||||
"description": "Un promemoria quotidiano per registrare come ti senti."
|
||||
},
|
||||
"lowStock": {
|
||||
"title": "Autonomia scorte",
|
||||
"description": "Ricevi un avviso prima che un farmaco finisca."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Suggerimento del coach",
|
||||
"description": "Stimoli proattivi dal coach IA."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Canali di notifica",
|
||||
"description": "APNs, Telegram, ntfy, Web Push, Webhook ed e-mail."
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": "Profilo",
|
||||
@@ -4773,7 +4803,8 @@
|
||||
"token": "Lo scambio del token con Oura è fallito. Riavvia la connessione.",
|
||||
"rate_limited": "Troppi tentativi di connessione. Attendi un momento e riprova.",
|
||||
"generic": "Errore sconosciuto durante la connessione. Riavvia la connessione."
|
||||
}
|
||||
},
|
||||
"integrationSetupGuide": "Guida alla configurazione"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administration",
|
||||
|
||||
+32
-1
@@ -4087,6 +4087,36 @@
|
||||
"mood": {
|
||||
"title": "Tagi nastroju",
|
||||
"description": "Grupy, tagi i kolejność w wyborze nastroju."
|
||||
},
|
||||
"reminders": {
|
||||
"title": "Przypomnienia i powiadomienia",
|
||||
"description": "Jedno miejsce dla każdego przypomnienia — o czym i kiedy — oraz kanały, które je dostarczają.",
|
||||
"categoriesGroup": "Przypomnienia",
|
||||
"channelsGroup": "Kanały dostarczania",
|
||||
"medication": {
|
||||
"title": "Przypomnienia o lekach",
|
||||
"description": "Godziny i okna dawek, ustawiane przy każdym leku."
|
||||
},
|
||||
"vorsorge": {
|
||||
"title": "Profilaktyka",
|
||||
"description": "Cykliczne badania i pomiary wraz z miejscem, gdzie się udać."
|
||||
},
|
||||
"mood": {
|
||||
"title": "Zapis nastroju",
|
||||
"description": "Codzienne przypomnienie, by zapisać samopoczucie."
|
||||
},
|
||||
"lowStock": {
|
||||
"title": "Zapas na wyczerpaniu",
|
||||
"description": "Otrzymaj ostrzeżenie, zanim lek się skończy."
|
||||
},
|
||||
"coach": {
|
||||
"title": "Podpowiedź trenera",
|
||||
"description": "Proaktywne wskazówki od trenera AI."
|
||||
},
|
||||
"channels": {
|
||||
"title": "Kanały powiadomień",
|
||||
"description": "APNs, Telegram, ntfy, Web Push, Webhook i e-mail."
|
||||
}
|
||||
}
|
||||
},
|
||||
"profile": "Profil",
|
||||
@@ -4773,7 +4803,8 @@
|
||||
"token": "Wymiana tokena z Oura nie powiodła się. Rozpocznij połączenie ponownie.",
|
||||
"rate_limited": "Zbyt wiele prób połączenia. Poczekaj chwilę i spróbuj ponownie.",
|
||||
"generic": "Nieznany błąd podczas łączenia. Rozpocznij połączenie ponownie."
|
||||
}
|
||||
},
|
||||
"integrationSetupGuide": "Przewodnik konfiguracji"
|
||||
},
|
||||
"admin": {
|
||||
"title": "Administracja",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -303,4 +303,43 @@ describe("IntegrationsSection — single-status-display contract (A5)", () => {
|
||||
// Fitbit / Google Health). The moodLog integration was removed.
|
||||
expect(count(html, 'data-testid="integration-card-divider"')).toBe(3);
|
||||
});
|
||||
|
||||
// v1.17.1 — every integration card carries the same discreet "Setup guide"
|
||||
// doc-link pointing at its runbook under the single shared docs base. The
|
||||
// affordance is one family across all six providers.
|
||||
it("every integration card carries the shared Setup-guide doc-link", () => {
|
||||
setIntegrationStatus({
|
||||
threshold: 3,
|
||||
integrations: [
|
||||
{
|
||||
integration: "withings",
|
||||
state: "connected",
|
||||
lastSuccessAt: "2026-05-09T18:00:00.000Z",
|
||||
lastAttemptAt: "2026-05-09T18:00:00.000Z",
|
||||
lastError: null,
|
||||
connected: true,
|
||||
configured: true,
|
||||
legacyLastSyncedAt: "2026-05-09T18:00:00.000Z",
|
||||
hasActivityScope: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const html = render();
|
||||
// One setup-guide link per card → six providers.
|
||||
expect(count(html, 'data-slot="integration-setup-guide"')).toBe(6);
|
||||
for (const provider of [
|
||||
"withings",
|
||||
"whoop",
|
||||
"fitbit",
|
||||
"polar",
|
||||
"oura",
|
||||
"nightscout",
|
||||
]) {
|
||||
expect(html).toContain(`data-testid="${provider}-setup-guide"`);
|
||||
expect(html).toContain(
|
||||
`href="https://docs.healthlog.dev/integrations/${provider}"`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
pillStateFor,
|
||||
type IntegrationStatusViewModel,
|
||||
} from "./shared";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
|
||||
export function FitbitCard({
|
||||
viewModel,
|
||||
@@ -484,6 +485,8 @@ export function FitbitCard({
|
||||
{t("settings.fitbitNoCredentials")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="fitbit" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -39,6 +39,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, measurementDependentKeys, queryKeys } from "@/lib/query-keys";
|
||||
|
||||
import { IntegrationErrorMessage } from "./shared";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
|
||||
interface NightscoutStatus {
|
||||
connected: boolean;
|
||||
@@ -366,6 +367,8 @@ export function NightscoutCard({ enabled = true }: { enabled?: boolean }) {
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="nightscout" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -37,6 +37,7 @@ import { TestConnectionButton } from "@/components/settings/test-connection-butt
|
||||
import { apiFetchRaw, apiGet, apiPost } from "@/lib/api/api-fetch";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { invalidateKeys, measurementDependentKeys } from "@/lib/query-keys";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
|
||||
export interface OAuthProviderStatus {
|
||||
connected: boolean;
|
||||
@@ -398,6 +399,8 @@ export function OAuthProviderCard({
|
||||
{msg}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider={provider} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* v1.17.1 — shared "Setup guide" doc-link for every Settings → Integrations
|
||||
* card. Each card paints the same affordance: one discreet, external link to
|
||||
* the provider's setup runbook ("was eingeben, wo klicken"). Single-sourced
|
||||
* here so all six cards (WHOOP / Withings / Fitbit / Polar / Oura / Nightscout)
|
||||
* read as one family and the docs host lives in exactly one place.
|
||||
*
|
||||
* The runbooks themselves are authored separately; this link wires the
|
||||
* destination now so a user who is mid-setup always knows where to go.
|
||||
*/
|
||||
|
||||
import { ExternalLink } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* Base for every provider setup runbook. The provider key is appended as a
|
||||
* path segment, e.g. `https://docs.healthlog.dev/integrations/whoop`. Kept as
|
||||
* a single constant so the host never drifts across cards.
|
||||
*/
|
||||
export const INTEGRATION_DOCS_BASE =
|
||||
"https://docs.healthlog.dev/integrations";
|
||||
|
||||
export type IntegrationDocsProvider =
|
||||
| "whoop"
|
||||
| "withings"
|
||||
| "fitbit"
|
||||
| "polar"
|
||||
| "oura"
|
||||
| "nightscout";
|
||||
|
||||
export function integrationDocsHref(provider: IntegrationDocsProvider): string {
|
||||
return `${INTEGRATION_DOCS_BASE}/${provider}`;
|
||||
}
|
||||
|
||||
export function IntegrationSetupGuideLink({
|
||||
provider,
|
||||
}: {
|
||||
provider: IntegrationDocsProvider;
|
||||
}) {
|
||||
const { t } = useTranslations();
|
||||
return (
|
||||
<a
|
||||
href={integrationDocsHref(provider)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-testid={`${provider}-setup-guide`}
|
||||
data-slot="integration-setup-guide"
|
||||
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1 text-xs underline-offset-2 hover:underline"
|
||||
>
|
||||
{t("settings.integrationSetupGuide")}
|
||||
<ExternalLink className="h-3 w-3" aria-hidden="true" />
|
||||
</a>
|
||||
);
|
||||
}
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
pillStateFor,
|
||||
type IntegrationStatusViewModel,
|
||||
} from "./shared";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
|
||||
export function WhoopCard({
|
||||
viewModel,
|
||||
@@ -456,6 +457,8 @@ export function WhoopCard({
|
||||
{t("settings.whoopNoCredentials")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="whoop" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
pillStateFor,
|
||||
type IntegrationStatusViewModel,
|
||||
} from "./shared";
|
||||
import { IntegrationSetupGuideLink } from "./setup-guide-link";
|
||||
|
||||
export function WithingsCard({
|
||||
viewModel,
|
||||
@@ -491,6 +492,8 @@ export function WithingsCard({
|
||||
{t("settings.withingsNoCredentials")}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<IntegrationSetupGuideLink provider="withings" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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