diff --git a/app/favicon.ico b/app/favicon.ico index 718d6fe..942c769 100644 Binary files a/app/favicon.ico and b/app/favicon.ico differ diff --git a/components/habits/daily-tasks-countdown.tsx b/components/habits/daily-tasks-countdown.tsx new file mode 100644 index 0000000..e480764 --- /dev/null +++ b/components/habits/daily-tasks-countdown.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; + +import { + formatCountdownDuration, + getMillisecondsUntilLocalMidnight, + getNextLocalMidnight, +} from "@/lib/date-keys"; +import { cn } from "@/lib/utils"; + +type DailyTasksCountdownProps = { + className?: string; + onDayChange?: () => void; +}; + +export function DailyTasksCountdown({ + className, + onDayChange, +}: DailyTasksCountdownProps) { + const [remainingMs, setRemainingMs] = useState(() => + getMillisecondsUntilLocalMidnight(), + ); + const hasNotifiedRef = useRef(false); + + useEffect(() => { + const tick = () => { + const nextRemainingMs = getMillisecondsUntilLocalMidnight(); + setRemainingMs(nextRemainingMs); + + if (nextRemainingMs === 0) { + if (!hasNotifiedRef.current) { + hasNotifiedRef.current = true; + onDayChange?.(); + } + return; + } + + hasNotifiedRef.current = false; + }; + + tick(); + const intervalId = window.setInterval(tick, 1000); + + return () => { + window.clearInterval(intervalId); + }; + }, [onDayChange]); + + const resetsAtLabel = getNextLocalMidnight().toLocaleTimeString(undefined, { + hour: "numeric", + minute: "2-digit", + }); + + return ( +
+ Time remaining today{" "} + + {formatCountdownDuration(remainingMs)} + +
++ Tasks reset at midnight ({resetsAtLabel}) +
+{error}
: null} {coinMessage ? ( @@ -572,12 +595,10 @@ export function HabitTracker({ mode = "daily" }: HabitTrackerProps) { return ({error}
: null} {coinMessage ? ( diff --git a/lib/daily-quiz-storage.ts b/lib/daily-quiz-storage.ts index f4eac1c..62bb053 100644 --- a/lib/daily-quiz-storage.ts +++ b/lib/daily-quiz-storage.ts @@ -6,6 +6,7 @@ import { } from "@/lib/avatar-state"; import { notifyHabitPetDataUpdated } from "@/lib/app-events"; import { + formatCountdownDuration, getMillisecondsUntilLocalMidnight, getNextLocalMidnight, getTodayDateKey, @@ -36,12 +37,7 @@ export function getMillisecondsUntilNextDailyQuiz(from = new Date()) { } 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")}`; + return formatCountdownDuration(milliseconds); } export async function resetTodaysDailyQuiz() { diff --git a/lib/date-keys.ts b/lib/date-keys.ts index 5593d29..7013847 100644 --- a/lib/date-keys.ts +++ b/lib/date-keys.ts @@ -17,3 +17,12 @@ export function getNextLocalMidnight(from = new Date()) { export function getMillisecondsUntilLocalMidnight(from = new Date()) { return Math.max(0, getNextLocalMidnight(from).getTime() - from.getTime()); } + +export function formatCountdownDuration(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")}`; +}