Skip to content

Commit

Permalink
enable completing past habit
Browse files Browse the repository at this point in the history
  • Loading branch information
dohsimpson committed Jan 19, 2025
1 parent 7ca1744 commit 2bcbabc
Show file tree
Hide file tree
Showing 11 changed files with 359 additions and 134 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
20 changes: 7 additions & 13 deletions components/DailyOverview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -33,16 +33,13 @@ export default function DailyOverview({
const { completeHabit, undoComplete } = useHabits()
const [settings] = useAtom(settingsAtom)
const [dailyHabits, setDailyHabits] = useState<Habit[]>([])
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])

Expand Down Expand Up @@ -78,11 +75,8 @@ export default function DailyOverview({
<h3 className="font-semibold">Daily Habits</h3>
<Badge variant="secondary">
{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
</Badge>
Expand Down
Empty file added components/DebugPerformance.tsx
Empty file.
106 changes: 80 additions & 26 deletions components/HabitCalendar.tsx
Original file line number Diff line number Diff line change
@@ -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<DateTime>(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 (
<div className="container mx-auto px-4 py-8">
Expand All @@ -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',
Expand All @@ -61,21 +79,57 @@ export default function HabitCalendar() {
<CardContent>
{selectedDate && (
<ul className="space-y-2">
{habits.map((habit) => {
const isCompleted = getHabitsForDate(selectedDate.toJSDate()).some((h: Habit) => h.id === habit.id)
return (
<li key={habit.id} className="flex items-center justify-between">
<span>
<Linkify>{habit.name}</Linkify>
</span>
{isCompleted ? (
<Badge variant="default">Completed</Badge>
) : (
<Badge variant="secondary">Not Completed</Badge>
)}
</li>
)
})}
{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 (
<li key={habit.id} className="flex items-center justify-between gap-2">
<span>
<Linkify>{habit.name}</Linkify>
</span>
<div className="flex items-center gap-2">
<div className="flex items-center gap-2">
{habit.targetCompletions && (
<span className="text-sm text-muted-foreground">
{completionsToday}/{habit.targetCompletions}
</span>
)}
<button
onClick={() => handleCompletePastHabit(habit, selectedDate)}
disabled={isCompleted}
className="relative h-4 w-4 hover:opacity-70 transition-opacity disabled:opacity-100"
>
{isCompleted ? (
<CircleCheck className="h-4 w-4 text-green-500" />
) : (
<div className="relative h-4 w-4">
<Circle className="absolute h-4 w-4 text-muted-foreground" />
<div
className="absolute h-4 w-4 rounded-full overflow-hidden"
style={{
background: `conic-gradient(
currentColor ${(completionsToday / (habit.targetCompletions ?? 1)) * 360}deg,
transparent ${(completionsToday / (habit.targetCompletions ?? 1)) * 360}deg 360deg
)`,
mask: 'radial-gradient(transparent 50%, black 51%)',
WebkitMask: 'radial-gradient(transparent 50%, black 51%)'
}}
/>
</div>
)}
</button>
</div>
</div>
</li>
)
})}
</ul>
)}
</CardContent>
Expand Down
18 changes: 13 additions & 5 deletions components/HabitItem.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -143,6 +141,16 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => {
setPomo((prev) => ({
...prev,
show: true,
selectedHabitId: habit.id
}))
}}>
<Timer className="mr-2 h-4 w-4" />
<span>Start Pomodoro</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onEdit} className="sm:hidden">
<Edit className="mr-2 h-4 w-4" />
Edit
Expand Down
65 changes: 0 additions & 65 deletions components/calendar.tsx

This file was deleted.

Loading

0 comments on commit 2bcbabc

Please sign in to comment.