mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
fix(notifications): repoint channel CTA, add error/loading primitives, cross-link surfaces
- The 'no channels' CTA now points at Settings → Integrations#channels (where channels are actually configured since v1.25.7) instead of the reminder-types screen, which had become a dead-end. - A failed load renders the shared QueryErrorCard with Retry; the loading frame reserves a matrix skeleton instead of a lone spinner. - The globally-disabled hint reads as a full sentence rather than the bare '(Disabled)' inline tag. - The three notification surfaces (per-event matrix, reminder types, delivery channels) now cross-link each other. - noChannels copy names its destination.
This commit is contained in:
1 parent
2dc9947de2
commit
2e84498cf3
10 files changed
+273
-37
No files matched your search
+10
-4
@@ -4055,14 +4055,17 @@
|
||||
"connectionsHeading": "Verbindungen",
|
||||
"connectionsDescription": "Datenquellen und Geräte verbinden und verwalten.",
|
||||
"channelsHeading": "Zustellkanäle",
|
||||
"channelsDescription": "Wohin Erinnerungen und Hinweise zugestellt werden."
|
||||
"channelsDescription": "Wohin Erinnerungen und Hinweise zugestellt werden.",
|
||||
"matrixLink": "Benachrichtigungen je Kanal"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Benachrichtigungen",
|
||||
"description": "Alle Erinnerungen an einem Ort — was wann erinnert wird und wohin es zugestellt wird.",
|
||||
"subtitle": "Wähle, welche Erinnerungen du erhältst und wohin sie zugestellt werden.",
|
||||
"remindersHeading": "Erinnerungen",
|
||||
"remindersDescription": "Was du erhältst."
|
||||
"remindersDescription": "Was du erhältst.",
|
||||
"matrixLink": "Benachrichtigungen je Kanal",
|
||||
"channelsLink": "Zustellkanäle"
|
||||
},
|
||||
"layout": {
|
||||
"title": "Darstellung",
|
||||
@@ -6268,7 +6271,7 @@
|
||||
"eventColumn": "Ereignis",
|
||||
"subtitle": "Wähle aus, welche Benachrichtigungen du über welchen Kanal erhalten möchtest.",
|
||||
"loadError": "Benachrichtigungseinstellungen konnten nicht geladen werden.",
|
||||
"noChannels": "Konfiguriere zuerst einen Benachrichtigungskanal in den Einstellungen.",
|
||||
"noChannels": "Konfiguriere zuerst einen Benachrichtigungskanal unter Einstellungen → Integrationen.",
|
||||
"goToSettings": "Zu den Kanal-Einstellungen",
|
||||
"channelDisabled": "Deaktiviert",
|
||||
"admin": {
|
||||
@@ -6379,7 +6382,10 @@
|
||||
"newDeviceBody": "Eine neue Anmeldung von {device} ({location}). Falls du das nicht warst, ändere sofort dein Passwort.",
|
||||
"unknownLocation": "unbekannter Ort"
|
||||
},
|
||||
"loginRequired": "Bitte melde dich an, um Benachrichtigungen zu verwalten."
|
||||
"loginRequired": "Bitte melde dich an, um Benachrichtigungen zu verwalten.",
|
||||
"someChannelsGloballyDisabled": "Ein oder mehrere Kanäle sind in den Einstellungen deaktiviert und erhalten keine Benachrichtigungen.",
|
||||
"reminderSettingsLink": "Erinnerungseinstellungen",
|
||||
"channelsLink": "Kanäle"
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+10
-4
@@ -4055,14 +4055,17 @@
|
||||
"connectionsHeading": "Connections",
|
||||
"connectionsDescription": "Connect and manage your data sources and devices.",
|
||||
"channelsHeading": "Delivery channels",
|
||||
"channelsDescription": "Where reminders and alerts are delivered."
|
||||
"channelsDescription": "Where reminders and alerts are delivered.",
|
||||
"matrixLink": "Per-channel notifications"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"description": "Every reminder in one place — what gets reminded and when, and where it's delivered.",
|
||||
"subtitle": "Choose which reminders you receive and where they're delivered.",
|
||||
"remindersHeading": "Reminders",
|
||||
"remindersDescription": "What you receive."
|
||||
"remindersDescription": "What you receive.",
|
||||
"matrixLink": "Per-channel notifications",
|
||||
"channelsLink": "Delivery channels"
|
||||
},
|
||||
"layout": {
|
||||
"title": "Appearance",
|
||||
@@ -6268,7 +6271,7 @@
|
||||
"eventColumn": "Event",
|
||||
"subtitle": "Choose which notifications you want to receive through which channel.",
|
||||
"loadError": "Notification preferences could not be loaded.",
|
||||
"noChannels": "Configure a notification channel in Settings first.",
|
||||
"noChannels": "Configure a notification channel in Settings → Integrations first.",
|
||||
"goToSettings": "Go to channel settings",
|
||||
"channelDisabled": "Disabled",
|
||||
"admin": {
|
||||
@@ -6379,7 +6382,10 @@
|
||||
"newDeviceBody": "A new sign-in from {device} ({location}). If this wasn't you, change your password immediately.",
|
||||
"unknownLocation": "unknown location"
|
||||
},
|
||||
"loginRequired": "Please sign in to manage notifications."
|
||||
"loginRequired": "Please sign in to manage notifications.",
|
||||
"someChannelsGloballyDisabled": "One or more channels are turned off in Settings and won’t receive notifications.",
|
||||
"reminderSettingsLink": "Reminder settings",
|
||||
"channelsLink": "Channels"
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+10
-4
@@ -4055,14 +4055,17 @@
|
||||
"connectionsHeading": "Conexiones",
|
||||
"connectionsDescription": "Conecta y gestiona tus fuentes de datos y dispositivos.",
|
||||
"channelsHeading": "Canales de entrega",
|
||||
"channelsDescription": "Dónde se entregan los recordatorios y avisos."
|
||||
"channelsDescription": "Dónde se entregan los recordatorios y avisos.",
|
||||
"matrixLink": "Notificaciones por canal"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notificaciones",
|
||||
"description": "Todos los recordatorios en un solo lugar: qué se recuerda y cuándo, y dónde se entrega.",
|
||||
"subtitle": "Elige qué recordatorios recibes y dónde se entregan.",
|
||||
"remindersHeading": "Recordatorios",
|
||||
"remindersDescription": "Lo que recibes."
|
||||
"remindersDescription": "Lo que recibes.",
|
||||
"matrixLink": "Notificaciones por canal",
|
||||
"channelsLink": "Canales de entrega"
|
||||
},
|
||||
"layout": {
|
||||
"title": "Apariencia",
|
||||
@@ -6268,7 +6271,7 @@
|
||||
"eventColumn": "Evento",
|
||||
"subtitle": "Elige qué notificaciones recibes y por qué canal.",
|
||||
"loadError": "No se han podido cargar los ajustes de notificación.",
|
||||
"noChannels": "Configura primero un canal de notificación en los ajustes.",
|
||||
"noChannels": "Configura primero un canal de notificación en Ajustes → Integraciones.",
|
||||
"goToSettings": "Ir a los ajustes del canal",
|
||||
"channelDisabled": "Desactivado",
|
||||
"admin": {
|
||||
@@ -6379,7 +6382,10 @@
|
||||
"newDeviceBody": "Un nuevo inicio de sesión desde {device} ({location}). Si no fuiste tú, cambia tu contraseña de inmediato.",
|
||||
"unknownLocation": "ubicación desconocida"
|
||||
},
|
||||
"loginRequired": "Inicia sesión para gestionar las notificaciones."
|
||||
"loginRequired": "Inicia sesión para gestionar las notificaciones.",
|
||||
"someChannelsGloballyDisabled": "Uno o más canales están desactivados en los ajustes y no recibirán notificaciones.",
|
||||
"reminderSettingsLink": "Ajustes de recordatorios",
|
||||
"channelsLink": "Canales"
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+10
-4
@@ -4055,14 +4055,17 @@
|
||||
"connectionsHeading": "Connexions",
|
||||
"connectionsDescription": "Connectez et gérez vos sources de données et appareils.",
|
||||
"channelsHeading": "Canaux de diffusion",
|
||||
"channelsDescription": "Où sont envoyés les rappels et alertes."
|
||||
"channelsDescription": "Où sont envoyés les rappels et alertes.",
|
||||
"matrixLink": "Notifications par canal"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"description": "Tous les rappels au même endroit — quoi et quand, et où c'est envoyé.",
|
||||
"subtitle": "Choisissez les rappels que vous recevez et où ils sont envoyés.",
|
||||
"remindersHeading": "Rappels",
|
||||
"remindersDescription": "Ce que vous recevez."
|
||||
"remindersDescription": "Ce que vous recevez.",
|
||||
"matrixLink": "Notifications par canal",
|
||||
"channelsLink": "Canaux de diffusion"
|
||||
},
|
||||
"layout": {
|
||||
"title": "Apparence",
|
||||
@@ -6268,7 +6271,7 @@
|
||||
"eventColumn": "Événement",
|
||||
"subtitle": "Choisis quelles notifications tu reçois et par quel canal.",
|
||||
"loadError": "Impossible de charger les paramètres de notification.",
|
||||
"noChannels": "Configure d’abord un canal de notification dans les paramètres.",
|
||||
"noChannels": "Configure d’abord un canal de notification dans Paramètres → Intégrations.",
|
||||
"goToSettings": "Aller aux paramètres du canal",
|
||||
"channelDisabled": "Désactivé",
|
||||
"admin": {
|
||||
@@ -6379,7 +6382,10 @@
|
||||
"newDeviceBody": "Une nouvelle connexion depuis {device} ({location}). Si ce n'était pas vous, changez votre mot de passe immédiatement.",
|
||||
"unknownLocation": "lieu inconnu"
|
||||
},
|
||||
"loginRequired": "Connecte-toi pour gérer les notifications."
|
||||
"loginRequired": "Connecte-toi pour gérer les notifications.",
|
||||
"someChannelsGloballyDisabled": "Un ou plusieurs canaux sont désactivés dans les paramètres et ne recevront pas de notifications.",
|
||||
"reminderSettingsLink": "Réglages des rappels",
|
||||
"channelsLink": "Canaux"
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+10
-4
@@ -4055,14 +4055,17 @@
|
||||
"connectionsHeading": "Connessioni",
|
||||
"connectionsDescription": "Collega e gestisci le tue fonti di dati e i dispositivi.",
|
||||
"channelsHeading": "Canali di recapito",
|
||||
"channelsDescription": "Dove vengono recapitati promemoria e avvisi."
|
||||
"channelsDescription": "Dove vengono recapitati promemoria e avvisi.",
|
||||
"matrixLink": "Notifiche per canale"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifiche",
|
||||
"description": "Tutti i promemoria in un unico posto — cosa e quando, e dove vengono recapitati.",
|
||||
"subtitle": "Scegli quali promemoria ricevere e dove vengono recapitati.",
|
||||
"remindersHeading": "Promemoria",
|
||||
"remindersDescription": "Ciò che ricevi."
|
||||
"remindersDescription": "Ciò che ricevi.",
|
||||
"matrixLink": "Notifiche per canale",
|
||||
"channelsLink": "Canali di recapito"
|
||||
},
|
||||
"layout": {
|
||||
"title": "Aspetto",
|
||||
@@ -6268,7 +6271,7 @@
|
||||
"eventColumn": "Evento",
|
||||
"subtitle": "Scegli quali notifiche ricevere e su quale canale.",
|
||||
"loadError": "Impossibile caricare le impostazioni di notifica.",
|
||||
"noChannels": "Configura prima un canale di notifica nelle impostazioni.",
|
||||
"noChannels": "Configura prima un canale di notifica in Impostazioni → Integrazioni.",
|
||||
"goToSettings": "Vai alle impostazioni del canale",
|
||||
"channelDisabled": "Disabilitato",
|
||||
"admin": {
|
||||
@@ -6379,7 +6382,10 @@
|
||||
"newDeviceBody": "Un nuovo accesso da {device} ({location}). Se non sei stato tu, cambia subito la password.",
|
||||
"unknownLocation": "posizione sconosciuta"
|
||||
},
|
||||
"loginRequired": "Accedi per gestire le notifiche."
|
||||
"loginRequired": "Accedi per gestire le notifiche.",
|
||||
"someChannelsGloballyDisabled": "Uno o più canali sono disattivati nelle impostazioni e non riceveranno notifiche.",
|
||||
"reminderSettingsLink": "Impostazioni promemoria",
|
||||
"channelsLink": "Canali"
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
+10
-4
@@ -4055,14 +4055,17 @@
|
||||
"connectionsHeading": "Połączenia",
|
||||
"connectionsDescription": "Połącz i zarządzaj źródłami danych oraz urządzeniami.",
|
||||
"channelsHeading": "Kanały dostarczania",
|
||||
"channelsDescription": "Gdzie dostarczane są przypomnienia i powiadomienia."
|
||||
"channelsDescription": "Gdzie dostarczane są przypomnienia i powiadomienia.",
|
||||
"matrixLink": "Powiadomienia wg kanału"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Powiadomienia",
|
||||
"description": "Wszystkie przypomnienia w jednym miejscu — co i kiedy oraz gdzie są dostarczane.",
|
||||
"subtitle": "Wybierz, które przypomnienia otrzymujesz i gdzie są dostarczane.",
|
||||
"remindersHeading": "Przypomnienia",
|
||||
"remindersDescription": "Co otrzymujesz."
|
||||
"remindersDescription": "Co otrzymujesz.",
|
||||
"matrixLink": "Powiadomienia wg kanału",
|
||||
"channelsLink": "Kanały dostarczania"
|
||||
},
|
||||
"layout": {
|
||||
"title": "Wygląd",
|
||||
@@ -6268,7 +6271,7 @@
|
||||
"eventColumn": "Zdarzenie",
|
||||
"subtitle": "Wybierz, które powiadomienia chcesz otrzymywać i którym kanałem.",
|
||||
"loadError": "Nie udało się załadować ustawień powiadomień.",
|
||||
"noChannels": "Najpierw skonfiguruj kanał powiadomień w ustawieniach.",
|
||||
"noChannels": "Najpierw skonfiguruj kanał powiadomień w Ustawienia → Integracje.",
|
||||
"goToSettings": "Przejdź do ustawień kanału",
|
||||
"channelDisabled": "Wyłączone",
|
||||
"admin": {
|
||||
@@ -6379,7 +6382,10 @@
|
||||
"newDeviceBody": "Nowe logowanie z {device} ({location}). Jeśli to nie Ty, natychmiast zmień hasło.",
|
||||
"unknownLocation": "nieznana lokalizacja"
|
||||
},
|
||||
"loginRequired": "Zaloguj się, aby zarządzać powiadomieniami."
|
||||
"loginRequired": "Zaloguj się, aby zarządzać powiadomieniami.",
|
||||
"someChannelsGloballyDisabled": "Co najmniej jeden kanał jest wyłączony w ustawieniach i nie będzie otrzymywać powiadomień.",
|
||||
"reminderSettingsLink": "Ustawienia przypomnień",
|
||||
"channelsLink": "Kanały"
|
||||
},
|
||||
"i18n": {
|
||||
"maintainershipBanner": {
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
|
||||
/**
|
||||
* Notification-matrix page contract (QoL/UX audit 2026-07-08):
|
||||
* - H2: the "no channels" CTA points at Settings → Integrations#channels,
|
||||
* the surface that can actually add a channel (not the reminder-types
|
||||
* screen, which no longer configures channels since v1.25.7).
|
||||
* - M3: a failed load renders the shared `QueryErrorCard` with a Retry,
|
||||
* never a bare alert box; the loading frame reserves a skeleton.
|
||||
* - L1: the globally-disabled hint reads as a full sentence, not the bare
|
||||
* "(Disabled)" inline tag.
|
||||
* - M7: the page cross-links the other two notification surfaces.
|
||||
*/
|
||||
|
||||
type AuthState = { isAuthenticated: boolean; isLoading: boolean };
|
||||
type QueryState = {
|
||||
data: unknown;
|
||||
isLoading: boolean;
|
||||
isError: boolean;
|
||||
};
|
||||
|
||||
let authState: AuthState = { isAuthenticated: true, isLoading: false };
|
||||
let queryState: QueryState = { data: undefined, isLoading: false, isError: false };
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
useAuth: () => authState,
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", () => ({
|
||||
useQuery: () => ({ ...queryState, refetch: vi.fn() }),
|
||||
useMutation: () => ({ mutate: vi.fn(), isPending: false }),
|
||||
useQueryClient: () => ({
|
||||
cancelQueries: vi.fn(),
|
||||
getQueryData: vi.fn(),
|
||||
setQueryData: vi.fn(),
|
||||
invalidateQueries: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
import NotificationsPage from "../page";
|
||||
|
||||
function render() {
|
||||
return renderToStaticMarkup(
|
||||
<I18nProvider initialLocale="en">
|
||||
<NotificationsPage />
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
authState = { isAuthenticated: true, isLoading: false };
|
||||
queryState = { data: undefined, isLoading: false, isError: false };
|
||||
});
|
||||
|
||||
describe("NotificationsPage", () => {
|
||||
it("M3: renders a skeleton while loading, not a lone spinner", () => {
|
||||
queryState.isLoading = true;
|
||||
const html = render();
|
||||
expect(html).toContain('data-slot="notifications-loading"');
|
||||
expect(html).not.toContain("animate-spin");
|
||||
});
|
||||
|
||||
it("M3: renders the shared QueryErrorCard with a retry on load failure", () => {
|
||||
queryState.isError = true;
|
||||
const html = render();
|
||||
expect(html).toContain('data-slot="query-error-card"');
|
||||
expect(html).toContain("Notification preferences could not be loaded.");
|
||||
});
|
||||
|
||||
it("H2 + L4: the no-channels CTA targets Settings → Integrations#channels", () => {
|
||||
queryState.data = { channels: [], preferences: [], eventTypes: [] };
|
||||
const html = render();
|
||||
expect(html).toContain('href="/settings/integrations#channels"');
|
||||
// The reminder-types screen no longer configures channels — never point there.
|
||||
expect(html).not.toContain('href="/settings/notifications"><');
|
||||
expect(html).toContain("Settings → Integrations");
|
||||
});
|
||||
|
||||
it("M7: the matrix cross-links the reminder-types + channels surfaces", () => {
|
||||
queryState.data = {
|
||||
channels: [
|
||||
{
|
||||
id: "c1",
|
||||
type: "telegram",
|
||||
label: "Telegram",
|
||||
enabled: true,
|
||||
globallyEnabled: true,
|
||||
},
|
||||
],
|
||||
preferences: [],
|
||||
eventTypes: ["MEDICATION_REMINDER"],
|
||||
};
|
||||
const html = render();
|
||||
expect(html).toContain('href="/settings/notifications"');
|
||||
expect(html).toContain('href="/settings/integrations#channels"');
|
||||
});
|
||||
|
||||
it("L1: the globally-disabled hint is a full sentence, not the bare tag", () => {
|
||||
queryState.data = {
|
||||
channels: [
|
||||
{
|
||||
id: "c1",
|
||||
type: "telegram",
|
||||
label: "Telegram",
|
||||
enabled: true,
|
||||
globallyEnabled: true,
|
||||
},
|
||||
{
|
||||
id: "c2",
|
||||
type: "ntfy",
|
||||
label: "ntfy",
|
||||
enabled: true,
|
||||
globallyEnabled: false,
|
||||
},
|
||||
],
|
||||
preferences: [],
|
||||
eventTypes: ["MEDICATION_REMINDER"],
|
||||
};
|
||||
const html = render();
|
||||
expect(html).toContain(
|
||||
"won’t receive notifications",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -7,8 +7,10 @@ import { Switch } from "@/components/ui/switch";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PageHeader } from "@/components/ui/page-header";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { QueryErrorCard } from "@/components/ui/query-error-card";
|
||||
import Link from "next/link";
|
||||
import { Bell, Loader2, Settings, AlertCircle } from "lucide-react";
|
||||
import { Bell, Settings, AlertCircle } from "lucide-react";
|
||||
import {
|
||||
EVENT_DEFAULT_ENABLED,
|
||||
type EventType,
|
||||
@@ -52,7 +54,7 @@ export default function NotificationsPage() {
|
||||
const { t } = useTranslations();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { data, isLoading, isError } = useQuery({
|
||||
const { data, isLoading, isError, refetch } = useQuery({
|
||||
queryKey: queryKeys.notificationsPreferences(),
|
||||
queryFn: async () => {
|
||||
return apiGet<PreferencesData>("/api/notifications/preferences");
|
||||
@@ -119,10 +121,50 @@ export default function NotificationsPage() {
|
||||
return EVENT_DEFAULT_ENABLED[eventType as EventType] ?? true;
|
||||
}
|
||||
|
||||
// Cross-links to the other two notification surfaces (reminder types +
|
||||
// delivery channels) so the matrix never reads as the only place to manage
|
||||
// notifications. Mirrored by back-links on those two settings screens.
|
||||
const headerActions = (
|
||||
<>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/settings/notifications">
|
||||
{t("notifications.reminderSettingsLink")}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/settings/integrations#channels">
|
||||
{t("notifications.channelsLink")}
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
if (authLoading || isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<Loader2 className="text-primary h-6 w-6 animate-spin motion-reduce:animate-none" />
|
||||
<div className="space-y-6">
|
||||
<PageHeader
|
||||
title={t("notifications.title")}
|
||||
description={t("notifications.subtitle")}
|
||||
/>
|
||||
{/* Skeleton mirrors the event-row matrix so the auth-resolving frame
|
||||
reserves layout instead of flashing a lone spinner. */}
|
||||
<div
|
||||
data-slot="notifications-loading"
|
||||
className="bg-card border-border overflow-hidden rounded-xl border"
|
||||
>
|
||||
{[0, 1, 2, 3, 4].map((i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="border-border flex items-center justify-between gap-4 border-b p-4 last:border-b-0"
|
||||
>
|
||||
<div className="min-w-0 flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-40 rounded" />
|
||||
<Skeleton className="h-3 w-56 rounded" />
|
||||
</div>
|
||||
<Skeleton className="h-5 w-9 shrink-0 rounded-full" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -149,12 +191,10 @@ export default function NotificationsPage() {
|
||||
title={t("notifications.title")}
|
||||
description={t("notifications.subtitle")}
|
||||
/>
|
||||
<div
|
||||
role="alert"
|
||||
className="border-destructive/40 bg-destructive/10 text-destructive rounded-lg border p-4 text-sm"
|
||||
>
|
||||
{t("notifications.loadError")}
|
||||
</div>
|
||||
<QueryErrorCard
|
||||
description={t("notifications.loadError")}
|
||||
onRetry={() => refetch()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -166,6 +206,7 @@ export default function NotificationsPage() {
|
||||
<PageHeader
|
||||
title={t("notifications.title")}
|
||||
description={t("notifications.subtitle")}
|
||||
actions={headerActions}
|
||||
/>
|
||||
<div className="bg-card border-border max-w-2xl rounded-xl border p-6">
|
||||
<div className="flex flex-col items-center gap-4 py-8 text-center">
|
||||
@@ -173,8 +214,10 @@ export default function NotificationsPage() {
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{t("notifications.noChannels")}
|
||||
</p>
|
||||
{/* Channel setup lives in Settings → Integrations since v1.25.7;
|
||||
point the CTA at that anchor, not the reminder-types screen. */}
|
||||
<Button asChild variant="outline">
|
||||
<Link href="/settings/notifications">
|
||||
<Link href="/settings/integrations#channels">
|
||||
<Settings className="h-4 w-4" />
|
||||
{t("notifications.goToSettings")}
|
||||
</Link>
|
||||
@@ -190,6 +233,7 @@ export default function NotificationsPage() {
|
||||
<PageHeader
|
||||
title={t("notifications.title")}
|
||||
description={t("notifications.subtitle")}
|
||||
actions={headerActions}
|
||||
/>
|
||||
|
||||
{/* Desktop: table layout */}
|
||||
@@ -321,12 +365,13 @@ export default function NotificationsPage() {
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Info hint about disabled channels */}
|
||||
{/* Info hint about globally-disabled channels — a full sentence that
|
||||
explains the state, not the bare "(Disabled)" inline tag. */}
|
||||
{channels.some((ch) => !ch.globallyEnabled) && (
|
||||
<div className="flex items-start gap-2 rounded-lg p-3 text-sm">
|
||||
<AlertCircle className="text-muted-foreground mt-0.5 h-4 w-4 shrink-0" />
|
||||
<p className="text-muted-foreground">
|
||||
{t("notifications.channelDisabled")}
|
||||
{t("notifications.someChannelsGloballyDisabled")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -21,12 +21,14 @@
|
||||
* `connections-panel.tsx` so existing unit-test imports keep resolving.
|
||||
*/
|
||||
|
||||
import Link from "next/link";
|
||||
import { Link2, Send } from "lucide-react";
|
||||
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { ConnectionsPanel } from "@/components/settings/integrations/connections-panel";
|
||||
import { NotificationChannelsPanel } from "@/components/settings/integrations/notification-channels-panel";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export {
|
||||
parseOAuthOutcome,
|
||||
@@ -65,6 +67,15 @@ export function IntegrationsSection() {
|
||||
title={t("settings.sections.integrations.channelsHeading")}
|
||||
description={t("settings.sections.integrations.channelsDescription")}
|
||||
/>
|
||||
{/* Names the per-event notification matrix so the three notification
|
||||
surfaces (matrix, reminder types, channels) cross-reference. */}
|
||||
<div>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/notifications">
|
||||
{t("settings.sections.integrations.matrixLink")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
<NotificationChannelsPanel />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Bell } from "lucide-react";
|
||||
|
||||
import { useAuth } from "@/hooks/use-auth";
|
||||
@@ -7,6 +8,7 @@ import { useTranslations } from "@/lib/i18n/context";
|
||||
import { SettingsCardHeader } from "@/components/settings/_card-header";
|
||||
import { LowStockCard } from "@/components/settings/low-stock-card";
|
||||
import { MoodReminderCard } from "@/components/settings/mood-reminder-card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
/**
|
||||
* `<NotificationsSection>` — Settings → Notifications ("Benachrichtigungen").
|
||||
@@ -44,6 +46,21 @@ export function NotificationsSection() {
|
||||
|
||||
return (
|
||||
<div className="space-y-10">
|
||||
{/* Cross-links to the other two notification surfaces so this screen
|
||||
names them — the per-event matrix and the delivery channels. */}
|
||||
<div className="flex flex-wrap gap-2" data-slot="notifications-crosslinks">
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/notifications">
|
||||
{t("settings.sections.notifications.matrixLink")}
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild variant="outline" size="sm">
|
||||
<Link href="/settings/integrations#channels">
|
||||
{t("settings.sections.notifications.channelsLink")}
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Group 1 — Reminders ("what you receive"). Rendered only when at
|
||||
least one reminder-type card is visible, so a fully-disabled
|
||||
account never shows an empty heading. */}
|
||||
|
||||
Reference in new issue
Block a user