From dea2b30c3bd1c8731a1a8af8309086243a30afeb Mon Sep 17 00:00:00 2001 From: dohsimpson Date: Fri, 21 Feb 2025 18:16:15 -0500 Subject: [PATCH] fix completion badge --- CHANGELOG.md | 11 +++ app/calendar/page.tsx | 1 + app/debug/habits/page.tsx | 70 +++++++++++++++++++ components/CompletionCountBadge.tsx | 51 +++++++------- components/DailyOverview.tsx | 66 +++++------------- components/HabitCalendar.tsx | 43 +++++------- components/Navigation.tsx | 4 +- components/ViewToggle.tsx | 2 +- lib/atoms.ts | 103 ++++++++++++++++++++-------- lib/types.ts | 6 ++ lib/utils.ts | 7 ++ package.json | 2 +- 12 files changed, 231 insertions(+), 135 deletions(-) create mode 100644 app/debug/habits/page.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index ec1b98a..74b1cd3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## Version 0.2.2 + +### Changed + +* persist "show all" settings in browser (#72) + +### Fixed + +* nav bar spacing +* completion count badge + ## Version 0.2.1 ### Changed diff --git a/app/calendar/page.tsx b/app/calendar/page.tsx index 7f39efd..8993563 100644 --- a/app/calendar/page.tsx +++ b/app/calendar/page.tsx @@ -1,6 +1,7 @@ import Layout from '@/components/Layout' import HabitCalendar from '@/components/HabitCalendar' import { ViewToggle } from '@/components/ViewToggle' +import CompletionCountBadge from '@/components/CompletionCountBadge' export default function CalendarPage() { return ( diff --git a/app/debug/habits/page.tsx b/app/debug/habits/page.tsx new file mode 100644 index 0000000..9ae7e84 --- /dev/null +++ b/app/debug/habits/page.tsx @@ -0,0 +1,70 @@ +'use client' + +import { useHabits } from "@/hooks/useHabits"; +import { habitsAtom, settingsAtom } from "@/lib/atoms"; +import { Habit } from "@/lib/types"; +import { useAtom } from "jotai"; +import { DateTime } from "luxon"; + + + +type CompletionCache = { + [dateKey: string]: { // dateKey format: "YYYY-MM-DD" + [habitId: string]: number // number of completions on that date + } +} + + +export default function DebugPage() { + const [habits] = useAtom(habitsAtom); + const [settings] = useAtom(settingsAtom); + + function buildCompletionCache(habits: Habit[], timezone: string): CompletionCache { + const cache: CompletionCache = {}; + + habits.forEach(habit => { + habit.completions.forEach(utcTimestamp => { + // Convert UTC timestamp to local date string in specified timezone + const localDate = DateTime + .fromISO(utcTimestamp) + .setZone(timezone) + .toFormat('yyyy-MM-dd'); + + if (!cache[localDate]) { + cache[localDate] = {}; + } + + // Increment completion count for this habit on this date + cache[localDate][habit.id] = (cache[localDate][habit.id] || 0) + 1; + }); + }); + + return cache; + } + + function getCompletedHabitsForDate( + habits: Habit[], + date: DateTime, + timezone: string, + completionCache: CompletionCache + ): Habit[] { + const dateKey = date.setZone(timezone).toFormat('yyyy-MM-dd'); + const dateCompletions = completionCache[dateKey] || {}; + + return habits.filter(habit => { + const completionsNeeded = habit.targetCompletions || 1; + const completionsAchieved = dateCompletions[habit.id] || 0; + return completionsAchieved >= completionsNeeded; + }); + } + + const habitCache = buildCompletionCache(habits.habits, settings.system.timezone); + + return ( +
+

Debug Page

+
+
+
+ ); +} \ No newline at end of file diff --git a/components/CompletionCountBadge.tsx b/components/CompletionCountBadge.tsx index 569e35a..688d1e4 100644 --- a/components/CompletionCountBadge.tsx +++ b/components/CompletionCountBadge.tsx @@ -1,40 +1,35 @@ -import { Badge } from '@/components/ui/badge' -import { Habit } from '@/lib/types' -import { isHabitDue, getCompletionsForDate } from '@/lib/utils' +import { Badge } from "@/components/ui/badge" +import { useAtom } from 'jotai' +import { completedHabitsMapAtom, habitsAtom, habitsByDateFamily } from '@/lib/atoms' +import { getTodayInTimezone } from '@/lib/utils' +import { useHabits } from '@/hooks/useHabits' +import { settingsAtom } from '@/lib/atoms' interface CompletionCountBadgeProps { - habits: Habit[] - selectedDate: luxon.DateTime - timezone: string - type: 'tasks' | 'habits' + type: 'habits' | 'tasks' + date?: string } -export function CompletionCountBadge({ habits, selectedDate, timezone, type }: CompletionCountBadgeProps) { - const filteredHabits = habits.filter(habit => { - const isTask = type === 'tasks' - if ((habit.isTask === isTask) && isHabitDue({ - habit, - timezone, - date: selectedDate - })) { - const completions = getCompletionsForDate({ habit, date: selectedDate, timezone }) - return completions >= (habit.targetCompletions || 1) - } - return false - }).length +export default function CompletionCountBadge({ + type, + date +}: CompletionCountBadgeProps) { + const [settings] = useAtom(settingsAtom) + const [completedHabitsMap] = useAtom(completedHabitsMapAtom) + const targetDate = date || getTodayInTimezone(settings.system.timezone) + const [dueHabits] = useAtom(habitsByDateFamily(targetDate)) - const totalHabits = habits.filter(habit => - (habit.isTask === (type === 'tasks')) && - isHabitDue({ - habit, - timezone, - date: selectedDate - }) + const completedCount = completedHabitsMap.get(targetDate)?.filter(h => + type === 'tasks' ? h.isTask : !h.isTask + ).length || 0 + + const totalCount = dueHabits.filter(h => + type === 'tasks' ? h.isTask : !h.isTask ).length return ( - {`${filteredHabits}/${totalHabits} Completed`} + {`${completedCount}/${totalCount} Completed`} ) } diff --git a/components/DailyOverview.tsx b/components/DailyOverview.tsx index b8bef36..4005a40 100644 --- a/components/DailyOverview.tsx +++ b/components/DailyOverview.tsx @@ -1,4 +1,5 @@ import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp, Timer, Plus } from 'lucide-react' +import CompletionCountBadge from './CompletionCountBadge' import { ContextMenu, ContextMenuContent, @@ -9,7 +10,7 @@ import { cn, isHabitDueToday, getHabitFreq } from '@/lib/utils' import Link from 'next/link' import { useState, useEffect } from 'react' import { useAtom } from 'jotai' -import { pomodoroAtom, settingsAtom, completedHabitsMapAtom, browserSettingsAtom, BrowserSettings, hasTasksAtom } from '@/lib/atoms' +import { pomodoroAtom, settingsAtom, completedHabitsMapAtom, browserSettingsAtom, BrowserSettings, hasTasksAtom, dailyHabitsAtom } 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' @@ -34,29 +35,15 @@ export default function DailyOverview({ }: UpcomingItemsProps) { const { completeHabit, undoComplete } = useHabits() const [settings] = useAtom(settingsAtom) - const [dailyHabits, setDailyHabits] = useState([]) - const [dailyTasks, setDailyTasks] = useState([]) const [completedHabitsMap] = useAtom(completedHabitsMapAtom) + const [dailyItems] = useAtom(dailyHabitsAtom) + const dailyTasks = dailyItems.filter(habit => habit.isTask) + const dailyHabits = dailyItems.filter(habit => !habit.isTask) const today = getTodayInTimezone(settings.system.timezone) const todayCompletions = completedHabitsMap.get(today) || [] const { saveHabit } = useHabits() const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom) - useEffect(() => { - // Filter habits and tasks that are due today and not archived - const filteredHabits = habits.filter(habit => - !habit.isTask && - !habit.archived && - isHabitDueToday({ habit, timezone: settings.system.timezone }) - ) - const filteredTasks = habits.filter(habit => - habit.isTask && - isHabitDueToday({ habit, timezone: settings.system.timezone }) - ) - setDailyHabits(filteredHabits) - setDailyTasks(filteredTasks) - }, [habits]) - // Get all wishlist items sorted by redeemable status (non-redeemable first) then by coin cost // Filter out archived wishlist items const sortedWishlistItems = wishlistItems @@ -74,9 +61,6 @@ export default function DailyOverview({ return a.coinCost - b.coinCost }) - const [expandedHabits, setExpandedHabits] = useState(false) - const [expandedTasks, setExpandedTasks] = useState(false) - const [expandedWishlist, setExpandedWishlist] = useState(false) const [hasTasks] = useAtom(hasTasksAtom) const [_, setPomo] = useAtom(pomodoroAtom) const [modalConfig, setModalConfig] = useState<{ @@ -126,13 +110,7 @@ export default function DailyOverview({

Daily Tasks

- - {`${dailyTasks.filter(task => { - const completions = (completedHabitsMap.get(today) || []) - .filter(h => h.id === task.id).length; - return completions >= (task.targetCompletions || 1); - }).length}/${dailyTasks.length} Completed`} - +
-
    +
      {dailyTasks .sort((a, b) => { // First by completion status @@ -177,7 +155,7 @@ export default function DailyOverview({ const bTarget = b.targetCompletions || 1; return bTarget - aTarget; }) - .slice(0, expandedTasks ? undefined : 5) + .slice(0, browserSettings.expandedTasks ? undefined : 5) .map((habit) => { 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 })) @@ -279,10 +257,10 @@ export default function DailyOverview({
    - - {`${dailyHabits.filter(habit => { - const completions = (completedHabitsMap.get(today) || []) - .filter(h => h.id === habit.id).length; - return completions >= (habit.targetCompletions || 1); - }).length}/${dailyHabits.length} Completed`} - +
    -
      +
        {dailyHabits .sort((a, b) => { // First by completion status @@ -388,7 +360,7 @@ export default function DailyOverview({ const bTarget = b.targetCompletions || 1; return bTarget - aTarget; }) - .slice(0, expandedHabits ? undefined : 5) + .slice(0, browserSettings.expandedHabits ? undefined : 5) .map((habit) => { 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 })) @@ -490,10 +462,10 @@ export default function DailyOverview({
      -
      +
      {sortedWishlistItems.length === 0 ? (
      No wishlist items yet. Add some goals to work towards! @@ -533,7 +505,7 @@ export default function DailyOverview({ ) : ( <> {sortedWishlistItems - .slice(0, expandedWishlist ? undefined : 5) + .slice(0, browserSettings.expandedWishlist ? undefined : 5) .map((item) => { const isRedeemable = item.coinCost <= coinBalance return ( @@ -587,10 +559,10 @@ export default function DailyOverview({