@@ -68,8 +83,12 @@ export function AvatarStatusCard() {
{hasCheckedInToday
- ? "Your pet is reacting to today's wellness check-in and journal."
- : "Complete today's quiz to update your pet's mood, energy, and health."}
+ ? completedTaskCount > 0
+ ? `Your pet reacts to today's quiz and ${completedTaskCount} completed task${completedTaskCount === 1 ? "" : "s"}. Meals, hydration, sleep, and relaxing habits boost health and mood.`
+ : "Your pet is reacting to today's wellness check-in. Complete habits to boost mood, energy, and health."
+ : completedTaskCount > 0
+ ? "Complete today's quiz for a full wellness read, then keep completing habits to boost your pet."
+ : "Complete today's quiz to update your pet's mood, energy, and health."}
{!hasCheckedInToday ? (
diff --git a/components/daily-quiz/daily-quiz-form.tsx b/components/daily-quiz/daily-quiz-form.tsx
index 2ad7ac4..f30b7df 100644
--- a/components/daily-quiz/daily-quiz-form.tsx
+++ b/components/daily-quiz/daily-quiz-form.tsx
@@ -1,7 +1,7 @@
"use client";
import { CheckCircle2 } from "lucide-react";
-import { useEffect, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { PixelAvatar } from "@/components/avatar/pixel-avatar";
import { WellnessSlider } from "@/components/daily-quiz/wellness-slider";
@@ -25,7 +25,9 @@ import {
type DailyQuizAnswers,
type DailyQuizSubmission,
} from "@/lib/avatar-state";
+import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
import {
+ getAvatarConditionForToday,
getDailyEntryForToday,
saveDailyEntry,
} from "@/lib/daily-quiz-storage";
@@ -42,6 +44,13 @@ export function DailyQuizForm() {
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState(null);
const [isReady, setIsReady] = useState(false);
+ const [liveCondition, setLiveCondition] = useState(
+ submission?.condition ?? null,
+ );
+
+ const refreshLiveCondition = useCallback(async () => {
+ setLiveCondition(await getAvatarConditionForToday());
+ }, []);
useEffect(() => {
async function loadDailyCheckIn() {
@@ -52,6 +61,9 @@ export function DailyQuizForm() {
setSubmission(existingEntry);
setAnswers(existingEntry.answers);
setJournal(existingEntry.journal);
+ setLiveCondition(existingEntry.condition);
+ } else {
+ await refreshLiveCondition();
}
} catch (loadError) {
setError(
@@ -65,7 +77,19 @@ export function DailyQuizForm() {
}
void loadDailyCheckIn();
- }, []);
+ }, [refreshLiveCondition]);
+
+ useEffect(() => {
+ const handleDataUpdated = () => {
+ void refreshLiveCondition();
+ };
+
+ window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, handleDataUpdated);
+
+ return () => {
+ window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, handleDataUpdated);
+ };
+ }, [refreshLiveCondition]);
const isCompleted = submission !== null;
const canSubmit = !isCompleted && !isSubmitting;
@@ -91,6 +115,7 @@ export function DailyQuizForm() {
try {
const savedEntry = await saveDailyEntry(answers, journal);
setSubmission(savedEntry);
+ setLiveCondition(savedEntry.condition);
} catch (submitError) {
setError(
submitError instanceof Error
@@ -137,28 +162,33 @@ export function DailyQuizForm() {
Your pet today
- Based on the wellness check-in you already submitted.
+ Based on your wellness check-in and completed habits today.
-
+
Mood
- {submission.condition.mood}
+ {(liveCondition ?? submission.condition).mood}
Energy
- {submission.condition.energy}/{WELLNESS_SCALE_MAX}
+ {(liveCondition ?? submission.condition).energy}/
+ {WELLNESS_SCALE_MAX}
Health
- {submission.condition.health}/{WELLNESS_SCALE_MAX}
+ {(liveCondition ?? submission.condition).health}/
+ {WELLNESS_SCALE_MAX}
diff --git a/components/habits/habit-tracker.tsx b/components/habits/habit-tracker.tsx
index 88ae39d..6e5df1c 100644
--- a/components/habits/habit-tracker.tsx
+++ b/components/habits/habit-tracker.tsx
@@ -30,6 +30,7 @@ import {
type Habit,
} from "@/lib/habits-storage";
import { formatCoinsDelta } from "@/lib/coins";
+import { describeHabitWellnessBoost } from "@/lib/habit-wellness-effects";
import { getProfilePreferences } from "@/lib/profile-preferences-storage";
import { hasCompletedDailyQuizToday } from "@/lib/daily-quiz-storage";
@@ -334,7 +335,14 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) {
delete next[habitId];
return next;
});
- setCoinMessage(formatCoinsDelta(coinsDelta));
+ setCoinMessage(
+ [
+ formatCoinsDelta(coinsDelta),
+ nextCompleted ? describeHabitWellnessBoost(habit.label) : null,
+ ]
+ .filter(Boolean)
+ .join(" ยท "),
+ );
} catch (toggleError) {
setOptimisticCompletion((current) => {
const next = { ...current };
diff --git a/components/profile/profile-preferences-form.tsx b/components/profile/profile-preferences-form.tsx
index f78880a..255b536 100644
--- a/components/profile/profile-preferences-form.tsx
+++ b/components/profile/profile-preferences-form.tsx
@@ -15,6 +15,16 @@ import {
import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import {
+ clearDailyReminderSentToday,
+ getNotificationPermissionLabel,
+ getNotificationPermissionState,
+ isNotificationSupported,
+ requestNotificationPermission,
+ showTestDailyReminderNotification,
+} from "@/lib/daily-reminders";
+import { getDailyReminderStatus } from "@/lib/daily-reminder-status";
+import { getTodayDateKey } from "@/lib/habits-storage";
import {
avatarVibeOptions,
defaultProfilePreferences,
@@ -35,6 +45,16 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
const [isReady, setIsReady] = useState(false);
const [error, setError] = useState(null);
const [isSaving, setIsSaving] = useState(false);
+ const [notificationPermission, setNotificationPermission] = useState(
+ getNotificationPermissionState(),
+ );
+ const [notificationMessage, setNotificationMessage] = useState(
+ null,
+ );
+
+ useEffect(() => {
+ setNotificationPermission(getNotificationPermissionState());
+ }, []);
useEffect(() => {
async function loadPreferences() {
@@ -63,6 +83,13 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
try {
const savedPreferences = await saveProfilePreferences(nextPreferences);
setPreferences(savedPreferences);
+
+ if (
+ "dailyReminderEnabled" in updates ||
+ "dailyReminderTime" in updates
+ ) {
+ clearDailyReminderSentToday(getTodayDateKey());
+ }
} catch (saveError) {
setPreferences(preferences);
setError(
@@ -75,6 +102,64 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
}
};
+ const handleDailyReminderToggle = async (checked: boolean) => {
+ if (checked) {
+ if (!isNotificationSupported()) {
+ setError("This browser does not support notifications.");
+ return;
+ }
+
+ const permission = await requestNotificationPermission();
+ setNotificationPermission(permission);
+
+ if (permission !== "granted") {
+ setError(
+ permission === "denied"
+ ? "Notifications are blocked. Enable them in your browser settings to use daily reminders."
+ : "Notification permission was not granted.",
+ );
+ return;
+ }
+ }
+
+ setError(null);
+ await updatePreferences({ dailyReminderEnabled: checked });
+ };
+
+ const handleTestNotification = async () => {
+ setNotificationMessage(null);
+ setError(null);
+
+ if (!isNotificationSupported()) {
+ setError("This browser does not support notifications.");
+ return;
+ }
+
+ const permission = await requestNotificationPermission();
+ setNotificationPermission(permission);
+
+ if (permission !== "granted") {
+ setError(
+ permission === "denied"
+ ? "Notifications are blocked. Enable them in your browser settings."
+ : "Notification permission was not granted.",
+ );
+ return;
+ }
+
+ const status = await getDailyReminderStatus();
+ const shown = showTestDailyReminderNotification(status);
+
+ if (!shown) {
+ setError("Could not show a test notification.");
+ return;
+ }
+
+ setNotificationMessage(
+ "Test notification sent. Check the corner of your screen or Windows notification center.",
+ );
+ };
+
if (!isReady) {
return (
Loading profile...
@@ -154,13 +239,19 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
Daily reminder
- void updatePreferences({ dailyReminderEnabled: checked === true })
+ void handleDailyReminderToggle(checked === true)
}
/>
+
+ Browser permission
+
+ {getNotificationPermissionLabel(notificationPermission)}
+
+
Daily reminder time
+
+ Reminders use your browser's local time and notify you once
+ per day when the quiz or habits are still incomplete. Keep this app
+ open in a tab for reminders to fire. On Windows, check Focus Assist
+ if notifications are hidden.
+
+ void handleTestNotification()}
+ >
+ Send test notification
+
+ {notificationMessage ? (
+ {notificationMessage}
+ ) : null}
diff --git a/components/reminders/daily-reminder-provider.tsx b/components/reminders/daily-reminder-provider.tsx
new file mode 100644
index 0000000..e2d3a3d
--- /dev/null
+++ b/components/reminders/daily-reminder-provider.tsx
@@ -0,0 +1,133 @@
+"use client";
+
+import { useCallback, useEffect, useRef, useState } from "react";
+
+import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
+import { getDailyReminderStatus } from "@/lib/daily-reminder-status";
+import {
+ getNotificationPermissionState,
+ getReminderCheckIntervalMs,
+ hasReachedReminderTime,
+ markDailyReminderSentToday,
+ showDailyReminderNotification,
+ wasDailyReminderSentToday,
+} from "@/lib/daily-reminders";
+import {
+ getProfilePreferences,
+ type ProfilePreferences,
+} from "@/lib/profile-preferences-storage";
+
+export function DailyReminderProvider() {
+ const [preferences, setPreferences] = useState