diff --git a/CHANGELOG.md b/CHANGELOG.md index bc86150..dd52101 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Version 0.1.22 + +### Added + +- start pomodoro from habit view +- complete past habit in calendar view (#32) + ## Version 0.1.21 ### Added diff --git a/components/DailyOverview.tsx b/components/DailyOverview.tsx index 461db04..e91bcfb 100644 --- a/components/DailyOverview.tsx +++ b/components/DailyOverview.tsx @@ -9,8 +9,8 @@ import { cn, isHabitDueToday, getHabitFreq } from '@/lib/utils' import Link from 'next/link' import { useState, useEffect } from 'react' import { useAtom } from 'jotai' -import { pomodoroAtom, settingsAtom } from '@/lib/atoms' -import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, getCompletedHabitsForDate, getCompletionsForDate } from '@/lib/utils' +import { pomodoroAtom, settingsAtom, completedHabitsMapAtom } from '@/lib/atoms' +import { getTodayInTimezone, isSameDate, t2d, d2t, getNow } from '@/lib/utils' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' import { Progress } from '@/components/ui/progress' @@ -33,16 +33,13 @@ export default function DailyOverview({ const { completeHabit, undoComplete } = useHabits() const [settings] = useAtom(settingsAtom) const [dailyHabits, setDailyHabits] = useState([]) + const [completedHabitsMap] = useAtom(completedHabitsMapAtom) const today = getTodayInTimezone(settings.system.timezone) - const todayCompletions = getCompletedHabitsForDate({ - habits, - date: getNow({ timezone: settings.system.timezone }), - timezone: settings.system.timezone - }) + const todayCompletions = completedHabitsMap.get(today) || [] useEffect(() => { // Filter habits that are due today based on their recurrence rule - const filteredHabits = habits.filter(habit => isHabitDueToday(habit, settings.system.timezone)) + const filteredHabits = habits.filter(habit => isHabitDueToday({ habit, timezone: settings.system.timezone })) setDailyHabits(filteredHabits) }, [habits]) @@ -78,11 +75,8 @@ export default function DailyOverview({

Daily Habits

{dailyHabits.filter(habit => { - const completions = getCompletionsForDate({ - habit, - date: today, - timezone: settings.system.timezone - }); + const completions = (completedHabitsMap.get(today) || []) + .filter(h => h.id === habit.id).length; return completions >= (habit.targetCompletions || 1); }).length}/{dailyHabits.length} Completed diff --git a/components/DebugPerformance.tsx b/components/DebugPerformance.tsx new file mode 100644 index 0000000..e69de29 diff --git a/components/HabitCalendar.tsx b/components/HabitCalendar.tsx index fc8af0f..30563a6 100644 --- a/components/HabitCalendar.tsx +++ b/components/HabitCalendar.tsx @@ -1,29 +1,42 @@ 'use client' -import { useState } from 'react' +import { useState, useMemo, useCallback } from 'react' import { Calendar } from '@/components/ui/calendar' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Badge } from '@/components/ui/badge' -import { d2s, getNow, t2d, getCompletedHabitsForDate } from '@/lib/utils' +import { Button } from '@/components/ui/button' +import { Check, Circle, CircleCheck } from 'lucide-react' +import { d2s, getNow, t2d, getCompletedHabitsForDate, isHabitDue, getISODate } from '@/lib/utils' import { useAtom } from 'jotai' -import { habitsAtom, settingsAtom } from '@/lib/atoms' +import { useHabits } from '@/hooks/useHabits' +import { habitsAtom, settingsAtom, completedHabitsMapAtom } from '@/lib/atoms' import { DateTime } from 'luxon' import Linkify from './linkify' import { Habit } from '@/lib/types' export default function HabitCalendar() { + const { completePastHabit } = useHabits() + + const handleCompletePastHabit = useCallback(async (habit: Habit, date: DateTime) => { + try { + await completePastHabit(habit, date) + } catch (error) { + console.error('Error completing past habit:', error) + } + }, [completePastHabit]) const [settings] = useAtom(settingsAtom) const [selectedDate, setSelectedDate] = useState(getNow({ timezone: settings.system.timezone })) const [habitsData] = useAtom(habitsAtom) const habits = habitsData.habits - const getHabitsForDate = (date: Date) => { - return getCompletedHabitsForDate({ - habits, - date: DateTime.fromJSDate(date), - timezone: settings.system.timezone - }) - } + const [completedHabitsMap] = useAtom(completedHabitsMapAtom) + + // Get completed dates for calendar modifiers + const completedDates = useMemo(() => { + return new Set(Array.from(completedHabitsMap.keys()).map(date => + getISODate({ dateTime: DateTime.fromISO(date), timezone: settings.system.timezone }) + )) + }, [completedHabitsMap, settings.system.timezone]) return (
@@ -40,7 +53,12 @@ export default function HabitCalendar() { onSelect={(e) => e && setSelectedDate(DateTime.fromJSDate(e))} className="rounded-md border" modifiers={{ - completed: (date) => getHabitsForDate(date).length > 0, + completed: (date) => completedDates.has( + getISODate({ + dateTime: DateTime.fromJSDate(date), + timezone: settings.system.timezone + })! + ) }} modifiersClassNames={{ completed: 'bg-green-100 text-green-800 font-bold', @@ -61,21 +79,57 @@ export default function HabitCalendar() { {selectedDate && (
    - {habits.map((habit) => { - const isCompleted = getHabitsForDate(selectedDate.toJSDate()).some((h: Habit) => h.id === habit.id) - return ( -
  • - - {habit.name} - - {isCompleted ? ( - Completed - ) : ( - Not Completed - )} -
  • - ) - })} + {habits + .filter(habit => isHabitDue({ + habit, + timezone: settings.system.timezone, + date: selectedDate + })) + .map((habit) => { + const habitsForDate = completedHabitsMap.get(getISODate({ dateTime: selectedDate, timezone: settings.system.timezone })) || [] + const completionsToday = habitsForDate.filter((h: Habit) => h.id === habit.id).length + const isCompleted = completionsToday >= (habit.targetCompletions || 1) + return ( +
  • + + {habit.name} + +
    +
    + {habit.targetCompletions && ( + + {completionsToday}/{habit.targetCompletions} + + )} + +
    +
    +
  • + ) + })}
)}
diff --git a/components/HabitItem.tsx b/components/HabitItem.tsx index 1edd8df..c233b7b 100644 --- a/components/HabitItem.tsx +++ b/components/HabitItem.tsx @@ -1,11 +1,10 @@ import { Habit } from '@/lib/types' import { useAtom } from 'jotai' -import { settingsAtom } from '@/lib/atoms' +import { settingsAtom, pomodoroAtom } from '@/lib/atoms' import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, parseNaturalLanguageRRule, parseRRule } from '@/lib/utils' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' -import ReactMarkdown from 'react-markdown' import { Button } from '@/components/ui/button' -import { Coins, Edit, Trash2, Check, Undo2, MoreVertical } from 'lucide-react' +import { Coins, Edit, Trash2, Check, Undo2, MoreVertical, Timer } from 'lucide-react' import { DropdownMenu, DropdownMenuContent, @@ -15,7 +14,6 @@ import { } from '@/components/ui/dropdown-menu' import { useEffect, useState } from 'react' import { useHabits } from '@/hooks/useHabits' -import { RRule } from 'rrule' import { INITIAL_RECURRENCE_RULE } from '@/lib/constants' interface HabitItemProps { @@ -27,7 +25,7 @@ interface HabitItemProps { export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) { const { completeHabit, undoComplete } = useHabits() const [settings] = useAtom(settingsAtom) - const today = getTodayInTimezone(settings.system.timezone) + const [_, setPomo] = useAtom(pomodoroAtom) const completionsToday = habit.completions?.filter(completion => isSameDate(t2d({ timestamp: completion, timezone: settings.system.timezone }), t2d({ timestamp: d2t({ dateTime: getNow({ timezone: settings.system.timezone }) }), timezone: settings.system.timezone })) ).length || 0 @@ -143,6 +141,16 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) { + { + setPomo((prev) => ({ + ...prev, + show: true, + selectedHabitId: habit.id + })) + }}> + + Start Pomodoro + Edit diff --git a/components/calendar.tsx b/components/calendar.tsx deleted file mode 100644 index ab2a056..0000000 --- a/components/calendar.tsx +++ /dev/null @@ -1,65 +0,0 @@ -"use client" - -import * as React from "react" -import { ChevronLeft, ChevronRight } from 'lucide-react' -import { DayPicker } from "react-day-picker" - -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" - -export type CalendarProps = React.ComponentProps - -function Calendar({ - className, - classNames, - showOutsideDays = true, - ...props -}: CalendarProps) { - return ( - , - IconRight: ({ ...props }) => , - }} - {...props} - /> - ) -} -Calendar.displayName = "Calendar" - -export { Calendar } - diff --git a/hooks/useHabits.tsx b/hooks/useHabits.tsx index bae369f..79346ca 100644 --- a/hooks/useHabits.tsx +++ b/hooks/useHabits.tsx @@ -2,7 +2,8 @@ import { useAtom } from 'jotai' import { habitsAtom, coinsAtom, settingsAtom } from '@/lib/atoms' import { addCoins, removeCoins, saveHabitsData } from '@/app/actions/data' import { Habit } from '@/lib/types' -import { getNowInMilliseconds, getTodayInTimezone, isSameDate, t2d, d2t, getNow, getCompletionsForDate } from '@/lib/utils' +import { DateTime } from 'luxon' +import { getNowInMilliseconds, getTodayInTimezone, isSameDate, t2d, d2t, getNow, getCompletionsForDate, getISODate, d2s } from '@/lib/utils' import { toast } from '@/hooks/use-toast' import { ToastAction } from '@/components/ui/toast' import { Undo2 } from 'lucide-react' @@ -161,10 +162,80 @@ export function useHabits() { return updatedHabits } + const completePastHabit = async (habit: Habit, date: DateTime) => { + const timezone = settings.system.timezone + const dateKey = getISODate({ dateTime: date, timezone }) + + // Check if already completed on this date + const completionsOnDate = habit.completions.filter(completion => + isSameDate(t2d({ timestamp: completion, timezone }), date) + ).length + const target = habit.targetCompletions || 1 + + if (completionsOnDate >= target) { + toast({ + title: "Already completed", + description: `This habit was already completed on ${d2s({ dateTime: date, timezone, format: 'yyyy-MM-dd' })}.`, + variant: "destructive", + }) + return null + } + + // Use current time but with the past date + const now = getNow({ timezone }) + const completionDateTime = date.set({ + hour: now.hour, + minute: now.minute, + second: now.second, + millisecond: now.millisecond + }) + const completionTimestamp = d2t({ dateTime: completionDateTime }) + const updatedHabit = { + ...habit, + completions: [...habit.completions, completionTimestamp] + } + + const updatedHabits = habitsData.habits.map(h => + h.id === habit.id ? updatedHabit : h + ) + + await saveHabitsData({ habits: updatedHabits }) + setHabitsData({ habits: updatedHabits }) + + // Check if we've now reached the target + const isTargetReached = completionsOnDate + 1 === target + if (isTargetReached) { + const updatedCoins = await addCoins( + habit.coinReward, + `Completed habit: ${habit.name} on ${d2s({ dateTime: date, timezone, format: 'yyyy-MM-dd' })}`, + 'HABIT_COMPLETION', + habit.id + ) + setCoins(updatedCoins) + } + + toast({ + title: isTargetReached ? "Habit completed!" : "Progress!", + description: isTargetReached + ? `You earned ${habit.coinReward} coins for ${dateKey}.` + : `You've completed ${completionsOnDate + 1}/${target} times on ${dateKey}.`, + action: undoComplete(updatedHabit)}> + Undo + + }) + + return { + updatedHabits, + newBalance: coins.balance, + newTransactions: coins.transactions + } + } + return { completeHabit, undoComplete, saveHabit, - deleteHabit + deleteHabit, + completePastHabit } } diff --git a/lib/atoms.ts b/lib/atoms.ts index 32ece17..714156d 100644 --- a/lib/atoms.ts +++ b/lib/atoms.ts @@ -15,7 +15,8 @@ import { calculateTotalSpent, calculateCoinsSpentToday, calculateTransactionsToday, - getCompletionsForToday + getCompletionsForToday, + getISODate } from "@/lib/utils"; export const settingsAtom = atom(getDefaultSettings()); @@ -72,6 +73,23 @@ export const pomodoroAtom = atom({ }) // Derived atom for today's completions of selected habit +export const completedHabitsMapAtom = atom((get) => { + const habits = get(habitsAtom).habits + const timezone = get(settingsAtom).system.timezone + + const map = new Map() + habits.forEach(habit => { + habit.completions.forEach(completion => { + const dateKey = getISODate({ dateTime: t2d({ timestamp: completion, timezone }), timezone }) + if (!map.has(dateKey)) { + map.set(dateKey, []) + } + map.get(dateKey)!.push(habit) + }) + }) + return map +}) + export const pomodoroTodayCompletionsAtom = atom((get) => { const pomo = get(pomodoroAtom) const habits = get(habitsAtom) @@ -79,7 +97,7 @@ export const pomodoroTodayCompletionsAtom = atom((get) => { if (!pomo.selectedHabitId) return 0 - const selectedHabit = habits.habits.find(h => h.id === pomo.selectedHabitId) + const selectedHabit = habits.habits.find(h => h.id === pomo.selectedHabitId!) if (!selectedHabit) return 0 return getCompletionsForToday({ diff --git a/lib/utils.test.ts b/lib/utils.test.ts index 120d985..3267ea3 100644 --- a/lib/utils.test.ts +++ b/lib/utils.test.ts @@ -14,7 +14,8 @@ import { calculateTotalEarned, calculateTotalSpent, calculateCoinsSpentToday, - isHabitDueToday + isHabitDueToday, + isHabitDue } from './utils' import { CoinTransaction } from './types' import { DateTime } from "luxon"; @@ -277,21 +278,21 @@ describe('isHabitDueToday', () => { DateTime.now = () => mockDate const habit = testHabit('FREQ=DAILY') - expect(isHabitDueToday(habit, 'UTC')).toBe(true) + expect(isHabitDueToday({ habit, timezone: 'UTC' })).toBe(true) }) test('should return true for weekly habit on correct day', () => { const habit = testHabit('FREQ=WEEKLY;BYDAY=MO') // Monday const mockDate = DateTime.fromISO('2024-01-01T00:00:00Z') as DateTime // Monday DateTime.now = () => mockDate - expect(isHabitDueToday(habit, 'UTC')).toBe(true) + expect(isHabitDueToday({ habit, timezone: 'UTC' })).toBe(true) }) test('should return false for weekly habit on wrong day', () => { const habit = testHabit('FREQ=WEEKLY;BYDAY=MO') // Monday const mockDate = DateTime.fromISO('2024-01-02T00:00:00Z') as DateTime // Tuesday DateTime.now = () => mockDate - expect(isHabitDueToday(habit, 'UTC')).toBe(false) + expect(isHabitDueToday({ habit, timezone: 'UTC' })).toBe(false) }) test('should handle timezones correctly', () => { @@ -339,7 +340,7 @@ describe('isHabitDueToday', () => { testCases.forEach(({ time, timezone, expected }) => { const mockDate = DateTime.fromISO(time) as DateTime DateTime.now = () => mockDate - expect(isHabitDueToday(habit, timezone)).toBe(expected) + expect(isHabitDueToday({ habit, timezone })).toBe(expected) }) }) @@ -368,7 +369,7 @@ describe('isHabitDueToday', () => { testCases.forEach(({ time, timezone, expected }) => { const mockDate = DateTime.fromISO(time) as DateTime DateTime.now = () => mockDate - expect(isHabitDueToday(habit, timezone)).toBe(expected) + expect(isHabitDueToday({ habit, timezone })).toBe(expected) }) }) @@ -396,7 +397,7 @@ describe('isHabitDueToday', () => { testCases.forEach(({ time, timezone, expected }) => { const mockDate = DateTime.fromISO(time) as DateTime DateTime.now = () => mockDate - expect(isHabitDueToday(habit, timezone)).toBe(expected) + expect(isHabitDueToday({ habit, timezone })).toBe(expected) }) }) @@ -424,7 +425,7 @@ describe('isHabitDueToday', () => { testCases.forEach(({ time, timezone, expected }) => { const mockDate = DateTime.fromISO(time) as DateTime DateTime.now = () => mockDate - expect(isHabitDueToday(habit, timezone)).toBe(expected) + expect(isHabitDueToday({ habit, timezone })).toBe(expected) }) }) @@ -432,21 +433,21 @@ describe('isHabitDueToday', () => { const habit = testHabit('FREQ=MONTHLY;BYMONTHDAY=1') const mockDate = DateTime.fromISO('2024-01-01T00:00:00Z') as DateTime // 1st of month DateTime.now = () => mockDate - expect(isHabitDueToday(habit, 'UTC')).toBe(true) + expect(isHabitDueToday({ habit, timezone: 'UTC' })).toBe(true) }) test('should handle yearly recurrence', () => { const habit = testHabit('FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1') const mockDate = DateTime.fromISO('2024-01-01T00:00:00Z') as DateTime // Jan 1st DateTime.now = () => mockDate - expect(isHabitDueToday(habit, 'UTC')).toBe(true) + expect(isHabitDueToday({ habit, timezone: 'UTC' })).toBe(true) }) test('should handle complex recurrence rules', () => { const habit = testHabit('FREQ=WEEKLY;BYDAY=MO,WE,FR') const mockDate = DateTime.fromISO('2024-01-01T00:00:00Z') as DateTime // Monday DateTime.now = () => mockDate - expect(isHabitDueToday(habit, 'UTC')).toBe(true) + expect(isHabitDueToday({ habit, timezone: 'UTC' })).toBe(true) }) test('should return false for invalid recurrence rule', () => { @@ -455,8 +456,122 @@ describe('isHabitDueToday', () => { const consoleSpy = spyOn(console, 'error').mockImplementation(() => { }) // Expect the function to throw an error - expect(() => isHabitDueToday(habit, 'UTC')).toThrow() + expect(() => isHabitDueToday({ habit, timezone: 'UTC' })).toThrow() consoleSpy.mockRestore() }) }) + +describe('isHabitDue', () => { + const testHabit = (frequency: string): Habit => ({ + id: 'test-habit', + name: 'Test Habit', + description: '', + frequency, + coinReward: 10, + completions: [] + }) + + test('should return true for daily habit on any date', () => { + const habit = testHabit('FREQ=DAILY') + const date = DateTime.fromISO('2024-01-01T12:34:56Z') + expect(isHabitDue({ habit, timezone: 'UTC', date })).toBe(true) + }) + + test('should return true for weekly habit on correct day', () => { + const habit = testHabit('FREQ=WEEKLY;BYDAY=MO') // Monday + const date = DateTime.fromISO('2024-01-01T00:00:00Z') // Monday + expect(isHabitDue({ habit, timezone: 'UTC', date })).toBe(true) + }) + + test('should return false for weekly habit on wrong day', () => { + const habit = testHabit('FREQ=WEEKLY;BYDAY=MO') // Monday + const date = DateTime.fromISO('2024-01-02T00:00:00Z') // Tuesday + expect(isHabitDue({ habit, timezone: 'UTC', date })).toBe(false) + }) + + test('should handle past dates correctly', () => { + const habit = testHabit('FREQ=WEEKLY;BYDAY=MO') // Monday + const pastDate = DateTime.fromISO('2023-12-25T00:00:00Z') // Christmas (Monday) + expect(isHabitDue({ habit, timezone: 'UTC', date: pastDate })).toBe(true) + }) + + test('should handle future dates correctly', () => { + const habit = testHabit('FREQ=WEEKLY;BYDAY=MO') // Monday + const futureDate = DateTime.fromISO('2024-12-30T00:00:00Z') // Monday + expect(isHabitDue({ habit, timezone: 'UTC', date: futureDate })).toBe(true) + }) + + test('should handle timezone transitions correctly', () => { + const habit = testHabit('FREQ=DAILY') + const testCases = [ + { + date: '2024-01-01T04:00:00Z', // UTC time that's still previous day in New York + timezone: 'America/New_York', + expected: true + }, + { + date: '2024-01-01T23:00:00Z', // Just before midnight in UTC + timezone: 'Asia/Tokyo', // Already next day in Tokyo + expected: true + }, + { + date: '2024-01-01T01:00:00Z', // Just after midnight in UTC + timezone: 'Pacific/Honolulu', // Still previous day in Hawaii + expected: true + } + ] + + testCases.forEach(({ date, timezone, expected }) => { + const dateObj = DateTime.fromISO(date) + expect(isHabitDue({ habit, timezone, date: dateObj })).toBe(expected) + }) + }) + + test('should handle daylight saving time transitions', () => { + const habit = testHabit('FREQ=DAILY') + const testCases = [ + { + date: '2024-03-10T02:30:00Z', // During DST transition in US + timezone: 'America/New_York', + expected: true + }, + { + date: '2024-10-27T01:30:00Z', // During DST transition in Europe + timezone: 'Europe/London', + expected: true + } + ] + + testCases.forEach(({ date, timezone, expected }) => { + const dateObj = DateTime.fromISO(date) + expect(isHabitDue({ habit, timezone, date: dateObj })).toBe(expected) + }) + }) + + test('should handle monthly recurrence', () => { + const habit = testHabit('FREQ=MONTHLY;BYMONTHDAY=1') + const date = DateTime.fromISO('2024-01-01T00:00:00Z') // 1st of month + expect(isHabitDue({ habit, timezone: 'UTC', date })).toBe(true) + }) + + test('should handle yearly recurrence', () => { + const habit = testHabit('FREQ=YEARLY;BYMONTH=1;BYMONTHDAY=1') + const date = DateTime.fromISO('2024-01-01T00:00:00Z') // Jan 1st + expect(isHabitDue({ habit, timezone: 'UTC', date })).toBe(true) + }) + + test('should handle complex recurrence rules', () => { + const habit = testHabit('FREQ=WEEKLY;BYDAY=MO,WE,FR') + const date = DateTime.fromISO('2024-01-01T00:00:00Z') // Monday + expect(isHabitDue({ habit, timezone: 'UTC', date })).toBe(true) + }) + + test('should return false for invalid recurrence rule', () => { + const habit = testHabit('INVALID_RRULE') + const date = DateTime.fromISO('2024-01-01T00:00:00Z') + const consoleSpy = spyOn(console, 'error').mockImplementation(() => { }) + expect(() => isHabitDue({ habit, timezone: 'UTC', date })).toThrow() + consoleSpy.mockRestore() + }) +}) diff --git a/lib/utils.ts b/lib/utils.ts index a1a9d99..27014ce 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -12,7 +12,11 @@ export function cn(...inputs: ClassValue[]) { // get today's date string for timezone export function getTodayInTimezone(timezone: string): string { const now = getNow({ timezone }); - return d2s({ dateTime: now, format: 'yyyy-MM-dd', timezone }); + return getISODate({ dateTime: now, timezone }); +} + +export function getISODate({ dateTime, timezone }: { dateTime: DateTime, timezone: string }): string { + return dateTime.setZone(timezone).toISODate()!; } // get datetime object of now @@ -200,26 +204,45 @@ export function serializeRRule(rrule: RRule) { return rrule.toString() } -export function isHabitDueToday(habit: Habit, timezone: string): boolean { - const startOfDay = DateTime.now().setZone(timezone).startOf('day') - const endOfDay = DateTime.now().setZone(timezone).endOf('day') +export function isHabitDue({ + habit, + timezone, + date +}: { + habit: Habit + timezone: string + date: DateTime +}): boolean { + const startOfDay = date.setZone(timezone).startOf('day') + const endOfDay = date.setZone(timezone).endOf('day') const ruleText = habit.frequency const rrule = parseRRule(ruleText) - rrule.origOptions.tzid = timezone // set the target timezone, rrule will do calculation in this timezone + rrule.origOptions.tzid = timezone rrule.options.tzid = rrule.origOptions.tzid - rrule.origOptions.dtstart = datetime(startOfDay.year, startOfDay.month, startOfDay.day, startOfDay.hour, startOfDay.minute, startOfDay.second) // set the start time to 00:00:00 of timezone's today + rrule.origOptions.dtstart = datetime(startOfDay.year, startOfDay.month, startOfDay.day, startOfDay.hour, startOfDay.minute, startOfDay.second) rrule.options.dtstart = rrule.origOptions.dtstart rrule.origOptions.count = 1 rrule.options.count = rrule.origOptions.count - const matches = rrule.all() // this is given as local time, we need to convert back to timezone time + const matches = rrule.all() if (!matches.length) return false - const t = DateTime.fromJSDate(matches[0]).toUTC().setZone('local', { keepLocalTime: true }).setZone(timezone) // this is the formula to convert local time matches[0] to tz time + const t = DateTime.fromJSDate(matches[0]).toUTC().setZone('local', { keepLocalTime: true }).setZone(timezone) return startOfDay <= t && t <= endOfDay } +export function isHabitDueToday({ + habit, + timezone +}: { + habit: Habit + timezone: string +}): boolean { + const today = getNow({ timezone }) + return isHabitDue({ habit, timezone, date: today }) +} + export function getHabitFreq(habit: Habit): Freq { const rrule = parseRRule(habit.frequency) const freq = rrule.origOptions.freq diff --git a/package.json b/package.json index 85015a5..b749466 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "habittrove", - "version": "0.1.21", + "version": "0.1.22", "private": true, "scripts": { "dev": "next dev --turbopack",