diff --git a/messages/de.json b/messages/de.json index 8084f470b..6eb93b9a6 100644 --- a/messages/de.json +++ b/messages/de.json @@ -2790,6 +2790,16 @@ }, "user": { "telegramTestBody": "HealthLog: Verbindung hergestellt. Telegram-Benachrichtigungen sind aktiv." + }, + "moodReminder": { + "title": "Tägliche Stimmungs-Erinnerung", + "description": "Sanfter Anstoß um 22:00 Uhr, falls du heute noch keine Stimmung erfasst hast. Greift nur auf bereits eingerichtete Kanäle (Telegram, ntfy, Web Push, Apple Push) zurück und schweigt, sobald du heute geloggt hast.", + "toggleAria": "Tägliche Stimmungs-Erinnerung umschalten", + "statusOn": "Aktiv um 22:00 deiner Zeit", + "statusOff": "Aus", + "enabledToast": "Stimmungs-Erinnerung aktiviert.", + "disabledToast": "Stimmungs-Erinnerung deaktiviert.", + "saveError": "Speichern fehlgeschlagen. Bitte später erneut versuchen." } }, "i18n": { diff --git a/messages/en.json b/messages/en.json index 84e4fea2d..df99c3f99 100644 --- a/messages/en.json +++ b/messages/en.json @@ -2790,6 +2790,16 @@ }, "user": { "telegramTestBody": "HealthLog: connection successful. Telegram notifications are active." + }, + "moodReminder": { + "title": "Daily mood reminder", + "description": "Gentle 22:00 nudge when you haven't logged a mood for the day. Routes only through channels you've already configured (Telegram, ntfy, Web Push, Apple Push) and stays silent once you've logged today.", + "toggleAria": "Toggle daily mood reminder", + "statusOn": "On at 22:00 your time", + "statusOff": "Off", + "enabledToast": "Mood reminder enabled.", + "disabledToast": "Mood reminder disabled.", + "saveError": "Save failed. Please try again." } }, "i18n": { diff --git a/messages/es.json b/messages/es.json index 65cb0d819..9503a3a0c 100644 --- a/messages/es.json +++ b/messages/es.json @@ -2790,6 +2790,16 @@ }, "user": { "telegramTestBody": "HealthLog: conexión correcta. Las notificaciones de Telegram están activas." + }, + "moodReminder": { + "title": "Recordatorio diario de ánimo", + "description": "Aviso suave a las 22:00 si aún no has registrado el ánimo del día. Se envía solo por canales ya configurados (Telegram, ntfy, Web Push, Apple Push) y permanece en silencio una vez que has registrado el ánimo hoy.", + "toggleAria": "Alternar recordatorio diario de ánimo", + "statusOn": "Activo a las 22:00 hora local", + "statusOff": "Desactivado", + "enabledToast": "Recordatorio de ánimo activado.", + "disabledToast": "Recordatorio de ánimo desactivado.", + "saveError": "No se pudo guardar. Inténtalo de nuevo." } }, "i18n": { diff --git a/messages/it.json b/messages/it.json index 12d69c952..c61b76936 100644 --- a/messages/it.json +++ b/messages/it.json @@ -2790,6 +2790,16 @@ }, "user": { "telegramTestBody": "HealthLog: connessione riuscita. Le notifiche Telegram sono attive." + }, + "moodReminder": { + "title": "Promemoria quotidiano dell’umore", + "description": "Promemoria leggero alle 22:00 se non hai ancora registrato l’umore di oggi. Usa solo i canali già configurati (Telegram, ntfy, Web Push, Apple Push) e tace una volta registrato l’umore di oggi.", + "toggleAria": "Attiva o disattiva il promemoria quotidiano dell’umore", + "statusOn": "Attivo alle 22:00 ora locale", + "statusOff": "Disattivato", + "enabledToast": "Promemoria dell’umore attivato.", + "disabledToast": "Promemoria dell’umore disattivato.", + "saveError": "Salvataggio non riuscito. Riprova." } }, "i18n": { diff --git a/messages/pl.json b/messages/pl.json index 7afc2bf37..7c485f648 100644 --- a/messages/pl.json +++ b/messages/pl.json @@ -2790,6 +2790,16 @@ }, "user": { "telegramTestBody": "HealthLog: połączenie udane. Powiadomienia Telegram są aktywne." + }, + "moodReminder": { + "title": "Codzienne przypomnienie o nastroju", + "description": "Delikatne przypomnienie o 22:00, jeśli dziś jeszcze nie zapisano nastroju. Wysyłane tylko skonfigurowanymi już kanałami (Telegram, ntfy, Web Push, Apple Push) i milknie, gdy dziś zapisano nastrój.", + "toggleAria": "Włącz lub wyłącz codzienne przypomnienie o nastroju", + "statusOn": "Aktywne o 22:00 czasu lokalnego", + "statusOff": "Wyłączone", + "enabledToast": "Przypomnienie o nastroju włączone.", + "disabledToast": "Przypomnienie o nastroju wyłączone.", + "saveError": "Zapis nieudany. Spróbuj ponownie." } }, "i18n": { diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts index bed82717a..60aff0fa9 100644 --- a/src/app/api/user/profile/route.ts +++ b/src/app/api/user/profile/route.ts @@ -35,6 +35,7 @@ export const GET = apiHandler(async () => { heightCm: true, locale: true, timezone: true, + moodReminderEnabled: true, }, }); @@ -47,6 +48,7 @@ export const GET = apiHandler(async () => { heightCm: dbUser?.heightCm ?? null, locale: dbUser?.locale ?? null, timezone: dbUser?.timezone ?? "Europe/Berlin", + moodReminderEnabled: dbUser?.moodReminderEnabled ?? false, }); }); @@ -72,5 +74,6 @@ export const PATCH = apiHandler(async (request: NextRequest) => { heightCm: result.user.heightCm, locale: result.user.locale, timezone: result.user.timezone, + moodReminderEnabled: result.user.moodReminderEnabled, }); }); diff --git a/src/components/settings/mood-reminder-card.tsx b/src/components/settings/mood-reminder-card.tsx new file mode 100644 index 000000000..2029a9d66 --- /dev/null +++ b/src/components/settings/mood-reminder-card.tsx @@ -0,0 +1,109 @@ +"use client"; + +import { useState } from "react"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { SmilePlus } from "lucide-react"; + +import { Switch } from "@/components/ui/switch"; +import { SettingsCardHeader } from "@/components/settings/_card-header"; +import { useTranslations } from "@/lib/i18n/context"; + +interface ProfileShape { + moodReminderEnabled: boolean; +} + +export function MoodReminderCard({ + isAuthenticated, +}: { + isAuthenticated: boolean; +}) { + const { t } = useTranslations(); + const queryClient = useQueryClient(); + const [optimistic, setOptimistic] = useState(null); + const [saving, setSaving] = useState(false); + const [msg, setMsg] = useState(null); + const [msgType, setMsgType] = useState<"success" | "error" | null>(null); + + const { data: profile } = useQuery({ + queryKey: ["user", "profile"], + queryFn: async () => { + const res = await fetch("/api/user/profile"); + if (!res.ok) throw new Error("Failed to load profile"); + return (await res.json()).data as ProfileShape; + }, + enabled: isAuthenticated, + }); + + const enabled = optimistic ?? profile?.moodReminderEnabled ?? false; + + async function handleToggle(next: boolean) { + setOptimistic(next); + setSaving(true); + setMsg(null); + setMsgType(null); + + const res = await fetch("/api/user/profile", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ moodReminderEnabled: next }), + }); + + if (res.ok) { + setMsg( + next + ? t("notifications.moodReminder.enabledToast") + : t("notifications.moodReminder.disabledToast"), + ); + setMsgType("success"); + await queryClient.invalidateQueries({ queryKey: ["user", "profile"] }); + setOptimistic(null); + } else { + setOptimistic(null); + setMsg(t("notifications.moodReminder.saveError")); + setMsgType("error"); + } + setSaving(false); + } + + return ( +
+ + + + {enabled + ? t("notifications.moodReminder.statusOn") + : t("notifications.moodReminder.statusOff")} + + + } + /> + {msg && ( +

+ {msg} +

+ )} +
+ ); +} diff --git a/src/components/settings/notifications-section.tsx b/src/components/settings/notifications-section.tsx index 0fc0a950e..2977b0b62 100644 --- a/src/components/settings/notifications-section.tsx +++ b/src/components/settings/notifications-section.tsx @@ -6,6 +6,7 @@ import { ChevronRight } from "lucide-react"; import { useAuth } from "@/hooks/use-auth"; import { useTranslations } from "@/lib/i18n/context"; +import { MoodReminderCard } from "@/components/settings/mood-reminder-card"; import { NotificationStatusCard } from "@/components/settings/notification-status-card"; import { NtfyCard } from "@/components/settings/ntfy-card"; import { TelegramCard } from "@/components/settings/telegram-card"; @@ -105,6 +106,9 @@ export function NotificationsSection() { )} +
+ +
); } diff --git a/src/lib/auth/profile-update.ts b/src/lib/auth/profile-update.ts index b8d74da65..50b7c59a6 100644 --- a/src/lib/auth/profile-update.ts +++ b/src/lib/auth/profile-update.ts @@ -18,6 +18,7 @@ const extendedProfileSchema = profileSchema.extend({ .max(64) .refine(isValidTimezone, "Invalid IANA timezone") .optional(), + moodReminderEnabled: z.boolean().optional(), }); export type ApplyProfileInput = z.infer; @@ -35,6 +36,7 @@ export interface ApplyProfileResult { gender: string | null; timezone: string; locale: string | null; + moodReminderEnabled: boolean; }; } @@ -91,6 +93,9 @@ export async function applyProfileUpdate( } if (data.locale !== undefined) updates.locale = data.locale; if (data.timezone !== undefined) updates.timezone = data.timezone; + if (data.moodReminderEnabled !== undefined) { + updates.moodReminderEnabled = data.moodReminderEnabled; + } // v1.4.18 — capture the prior locale so we can emit a separate // `settings.locale.update` audit row when the locale actually @@ -138,6 +143,7 @@ export async function applyProfileUpdate( gender: updatedUser.gender, timezone: updatedUser.timezone, locale: updatedUser.locale, + moodReminderEnabled: updatedUser.moodReminderEnabled, }, }; }