added demo quiz reset option. Added timer for next available daily quiz

This commit is contained in:
masongga
2026-05-24 07:48:51 -06:00
parent 4998576e64
commit 1a151b5c7c
6 changed files with 199 additions and 7 deletions

View File

@@ -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 (
<div className="space-y-1 pt-1">
<p className="text-sm text-muted-foreground">
Next check-in available in{" "}
<span className="font-medium tabular-nums text-foreground">
{formatDailyQuizCountdown(remainingMs)}
</span>
</p>
<p className="text-xs text-muted-foreground">
Opens at midnight ({nextAvailableLabel})
</p>
</div>
);
}

View File

@@ -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() {
</Link>{" "}
whenever you want to see how they are doing.
</p>
<DailyQuizCountdown
onAvailable={() => {
void prefetchQuizTabData({ force: true }).then(applyQuizData);
}}
/>
</div>
</CardContent>
</Card>

View File

@@ -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<string | null>(
null,
);
const [quizResetMessage, setQuizResetMessage] = useState<string | null>(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 (
<p className="text-sm text-muted-foreground">Loading profile...</p>
@@ -365,6 +389,33 @@ export function ProfilePreferencesForm({ email }: ProfilePreferencesFormProps) {
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Demo tools</CardTitle>
<CardDescription>
Shortcuts for testing flows without waiting for the daily reset.
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<p className="text-xs text-muted-foreground">
Reset clears today&apos;s wellness quiz entry so you can complete it
again immediately.
</p>
<Button
type="button"
variant="outline"
className="w-full"
disabled={isSaving || isResettingQuiz}
onClick={() => void handleResetDailyQuiz()}
>
{isResettingQuiz ? "Resetting..." : "Reset today's quiz"}
</Button>
{quizResetMessage ? (
<p className="text-xs text-emerald-600">{quizResetMessage}</p>
) : null}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Account settings</CardTitle>

View File

@@ -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(

19
lib/date-keys.ts Normal file
View File

@@ -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());
}

View File

@@ -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`);