diff --git a/components/daily-quiz/daily-quiz-countdown.tsx b/components/daily-quiz/daily-quiz-countdown.tsx
new file mode 100644
index 0000000..a79c437
--- /dev/null
+++ b/components/daily-quiz/daily-quiz-countdown.tsx
@@ -0,0 +1,67 @@
+"use client";
+
+import { useEffect, useRef, useState } from "react";
+
+import {
+ formatDailyQuizCountdown,
+ getMillisecondsUntilNextDailyQuiz,
+ getNextDailyQuizAvailableAt,
+} from "@/lib/daily-quiz-storage";
+
+type DailyQuizCountdownProps = {
+ onAvailable?: () => void;
+};
+
+export function DailyQuizCountdown({ onAvailable }: DailyQuizCountdownProps) {
+ const [remainingMs, setRemainingMs] = useState(() =>
+ getMillisecondsUntilNextDailyQuiz(),
+ );
+ const hasNotifiedRef = useRef(false);
+
+ useEffect(() => {
+ const tick = () => {
+ const nextRemainingMs = getMillisecondsUntilNextDailyQuiz();
+ setRemainingMs(nextRemainingMs);
+
+ if (nextRemainingMs === 0) {
+ if (!hasNotifiedRef.current) {
+ hasNotifiedRef.current = true;
+ onAvailable?.();
+ }
+ return;
+ }
+
+ hasNotifiedRef.current = false;
+ };
+
+ tick();
+ const intervalId = window.setInterval(tick, 1000);
+
+ return () => {
+ window.clearInterval(intervalId);
+ };
+ }, [onAvailable]);
+
+ const nextAvailableAt = getNextDailyQuizAvailableAt();
+ const nextAvailableLabel = nextAvailableAt.toLocaleString(undefined, {
+ weekday: "short",
+ month: "short",
+ day: "numeric",
+ hour: "numeric",
+ minute: "2-digit",
+ });
+
+ return (
+
+
+ Next check-in available in{" "}
+
+ {formatDailyQuizCountdown(remainingMs)}
+
+
+
+ Opens at midnight ({nextAvailableLabel})
+
+
+ );
+}
diff --git a/components/daily-quiz/daily-quiz-form.tsx b/components/daily-quiz/daily-quiz-form.tsx
index e75f1a2..315e13d 100644
--- a/components/daily-quiz/daily-quiz-form.tsx
+++ b/components/daily-quiz/daily-quiz-form.tsx
@@ -5,6 +5,7 @@ import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import { WellnessSlider } from "@/components/daily-quiz/wellness-slider";
+import { DailyQuizCountdown } from "@/components/daily-quiz/daily-quiz-countdown";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -28,6 +29,7 @@ import {
getCachedQuizTabData,
prefetchQuizTabData,
} from "@/lib/app-tab-data-cache";
+import { HABIT_PET_DATA_UPDATED_EVENT } from "@/lib/app-events";
import { saveDailyEntry } from "@/lib/daily-quiz-storage";
import { JOURNAL_MAX_LENGTH } from "@/lib/journal-safety";
import { routes } from "@/lib/routes";
@@ -63,7 +65,7 @@ export function DailyQuizForm() {
async function loadDailyCheckIn() {
try {
- const data = await prefetchQuizTabData();
+ const data = await prefetchQuizTabData({ force: true });
if (cancelled) {
return;
@@ -89,8 +91,15 @@ export function DailyQuizForm() {
void loadDailyCheckIn();
+ const handleDataUpdated = () => {
+ void loadDailyCheckIn();
+ };
+
+ window.addEventListener(HABIT_PET_DATA_UPDATED_EVENT, handleDataUpdated);
+
return () => {
cancelled = true;
+ window.removeEventListener(HABIT_PET_DATA_UPDATED_EVENT, handleDataUpdated);
};
}, [applyQuizData]);
@@ -161,6 +170,11 @@ export function DailyQuizForm() {
{" "}
whenever you want to see how they are doing.
+ {
+ void prefetchQuizTabData({ force: true }).then(applyQuizData);
+ }}
+ />
diff --git a/components/profile/profile-preferences-form.tsx b/components/profile/profile-preferences-form.tsx
index 4e175a1..656234e 100644
--- a/components/profile/profile-preferences-form.tsx
+++ b/components/profile/profile-preferences-form.tsx
@@ -25,6 +25,7 @@ import {
} from "@/lib/daily-reminders";
import { getDailyReminderStatus } from "@/lib/daily-reminder-status";
import { getTodayDateKey } from "@/lib/habits-storage";
+import { resetTodaysDailyQuiz } from "@/lib/daily-quiz-storage";
import {
reminderDeliveryOptions,
usesSystemReminders,
@@ -56,6 +57,8 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
const [notificationMessage, setNotificationMessage] = useState(
null,
);
+ const [quizResetMessage, setQuizResetMessage] = useState(null);
+ const [isResettingQuiz, setIsResettingQuiz] = useState(false);
useEffect(() => {
setNotificationPermission(getNotificationPermissionState());
@@ -209,6 +212,27 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
);
};
+ const handleResetDailyQuiz = async () => {
+ setQuizResetMessage(null);
+ setError(null);
+ setIsResettingQuiz(true);
+
+ try {
+ await resetTodaysDailyQuiz();
+ setQuizResetMessage(
+ "Today's quiz was reset. Open the Quiz tab to take it again.",
+ );
+ } catch (resetError) {
+ setError(
+ resetError instanceof Error
+ ? resetError.message
+ : "Could not reset today's quiz.",
+ );
+ } finally {
+ setIsResettingQuiz(false);
+ }
+ };
+
if (!isReady) {
return (
Loading profile...
@@ -365,6 +389,33 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
+
+
+ Demo tools
+
+ Shortcuts for testing flows without waiting for the daily reset.
+
+
+
+
+ Reset clears today's wellness quiz entry so you can complete it
+ again immediately.
+
+
+ {quizResetMessage ? (
+ {quizResetMessage}
+ ) : null}
+
+
+
Account settings
diff --git a/lib/daily-quiz-storage.ts b/lib/daily-quiz-storage.ts
index 417eb54..f4eac1c 100644
--- a/lib/daily-quiz-storage.ts
+++ b/lib/daily-quiz-storage.ts
@@ -5,6 +5,11 @@ import {
type DailyQuizSubmission,
} from "@/lib/avatar-state";
import { notifyHabitPetDataUpdated } from "@/lib/app-events";
+import {
+ getMillisecondsUntilLocalMidnight,
+ getNextLocalMidnight,
+ getTodayDateKey,
+} from "@/lib/date-keys";
import { computeAvatarConditionWithHabitBoosts } from "@/lib/habit-wellness-effects";
import { getCompletedHabitLabelsForToday } from "@/lib/habits-storage";
import { validateJournalEntry } from "@/lib/journal-safety";
@@ -20,8 +25,45 @@ type DailyEntryRow = {
journal: string | null;
};
-export function getTodayDateKey(date = new Date()) {
- return date.toISOString().slice(0, 10);
+export { getTodayDateKey };
+
+export function getNextDailyQuizAvailableAt(from = new Date()) {
+ return getNextLocalMidnight(from);
+}
+
+export function getMillisecondsUntilNextDailyQuiz(from = new Date()) {
+ return getMillisecondsUntilLocalMidnight(from);
+}
+
+export function formatDailyQuizCountdown(milliseconds: number) {
+ const totalSeconds = Math.max(0, Math.floor(milliseconds / 1000));
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
+ const seconds = totalSeconds % 60;
+
+ return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
+}
+
+export async function resetTodaysDailyQuiz() {
+ const userId = await getAuthenticatedUserId();
+
+ if (!userId) {
+ throw new Error("You must be signed in to reset today's quiz.");
+ }
+
+ const supabase = createClient();
+ const entryDate = getTodayDateKey();
+ const { error } = await supabase
+ .from("daily_entries")
+ .delete()
+ .eq("user_id", userId)
+ .eq("entry_date", entryDate);
+
+ if (error) {
+ throw new Error(error.message);
+ }
+
+ notifyHabitPetDataUpdated();
}
function mapRowToSubmission(
diff --git a/lib/date-keys.ts b/lib/date-keys.ts
new file mode 100644
index 0000000..5593d29
--- /dev/null
+++ b/lib/date-keys.ts
@@ -0,0 +1,19 @@
+export function getTodayDateKey(date = new Date()) {
+ return date.toLocaleDateString("en-CA");
+}
+
+export function getNextLocalMidnight(from = new Date()) {
+ return new Date(
+ from.getFullYear(),
+ from.getMonth(),
+ from.getDate() + 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ );
+}
+
+export function getMillisecondsUntilLocalMidnight(from = new Date()) {
+ return Math.max(0, getNextLocalMidnight(from).getTime() - from.getTime());
+}
diff --git a/lib/habits-storage.ts b/lib/habits-storage.ts
index ec13018..e6495f9 100644
--- a/lib/habits-storage.ts
+++ b/lib/habits-storage.ts
@@ -16,6 +16,9 @@ import {
getStreakMilestoneBonus,
} from "@/lib/coins";
import { createClient } from "@/lib/supabase/client";
+import { getTodayDateKey } from "@/lib/date-keys";
+
+export { getTodayDateKey };
export type Habit = {
id: string;
@@ -43,10 +46,6 @@ type HabitRow = {
const habitSelectFields =
"id, name, streak, last_completed_on, claimed_streak_milestones";
-export function getTodayDateKey(date = new Date()) {
- return date.toISOString().slice(0, 10);
-}
-
function getDayDiff(fromDate: string, toDate: string) {
const from = new Date(`${fromDate}T00:00:00`);
const to = new Date(`${toDate}T00:00:00`);