From d3502e284dc1b9f74a65c3ee7467434bfa5c43b8 Mon Sep 17 00:00:00 2001 From: dohsimpson Date: Wed, 22 Jan 2025 17:59:59 -0500 Subject: [PATCH] added support for tasks --- CHANGELOG.md | 6 ++++++ app/actions/data.ts | 36 +++++++++++++++++++++----------- components/AddEditHabitModal.tsx | 35 ++++++++++++++++++------------- components/DailyOverview.tsx | 13 ++++++++---- components/Dashboard.tsx | 2 +- components/HabitItem.tsx | 10 ++++++--- components/HabitList.tsx | 25 ++++++++++++++-------- components/Header.tsx | 4 +++- components/Navigation.tsx | 20 +++++++++++++----- components/ViewToggle.tsx | 27 ++++++++++++------------ hooks/useCoins.tsx | 14 +++++++++++-- hooks/useHabits.tsx | 36 ++++++++++++++++---------------- hooks/useWishlist.tsx | 12 +++++------ lib/atoms.ts | 17 ++++++++------- lib/constants.ts | 11 ++++++++++ lib/types.ts | 3 ++- lib/utils.ts | 33 +++++++++++++++++++++++++---- package-lock.json | 21 +++++++++++++++++-- package.json | 3 ++- 19 files changed, 223 insertions(+), 105 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 438db79..175d467 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Version 0.1.25 + +### Added + +- added support for tasks (#41) + ## Version 0.1.24 ### Fixed diff --git a/app/actions/data.ts b/app/actions/data.ts index 8e7ebff..c6671cf 100644 --- a/app/actions/data.ts +++ b/app/actions/data.ts @@ -101,13 +101,19 @@ export async function saveCoinsData(data: CoinsData): Promise { return saveData('coins', data) } -export async function addCoins( - amount: number, - description: string, - type: TransactionType = 'MANUAL_ADJUSTMENT', - relatedItemId?: string, +export async function addCoins({ + amount, + description, + type = 'MANUAL_ADJUSTMENT', + relatedItemId, + note, +}: { + amount: number + description: string + type?: TransactionType + relatedItemId?: string note?: string -): Promise { +}): Promise { const data = await loadCoinsData() const newTransaction: CoinTransaction = { id: crypto.randomUUID(), @@ -143,13 +149,19 @@ export async function saveSettings(settings: Settings): Promise { return saveData('settings', settings) } -export async function removeCoins( - amount: number, - description: string, - type: TransactionType = 'MANUAL_ADJUSTMENT', - relatedItemId?: string, +export async function removeCoins({ + amount, + description, + type = 'MANUAL_ADJUSTMENT', + relatedItemId, + note, +}: { + amount: number + description: string + type?: TransactionType + relatedItemId?: string note?: string -): Promise { +}): Promise { const data = await loadCoinsData() const newTransaction: CoinTransaction = { id: crypto.randomUUID(), diff --git a/components/AddEditHabitModal.tsx b/components/AddEditHabitModal.tsx index 279ea28..8e0a9c4 100644 --- a/components/AddEditHabitModal.tsx +++ b/components/AddEditHabitModal.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react' import { RRule, RRuleSet, rrulestr } from 'rrule' import { useAtom } from 'jotai' -import { settingsAtom } from '@/lib/atoms' +import { settingsAtom, browserSettingsAtom } from '@/lib/atoms' import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -16,8 +16,10 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import data from '@emoji-mart/data' import Picker from '@emoji-mart/react' import { Habit } from '@/lib/types' -import { parseNaturalLanguageRRule, parseRRule, serializeRRule } from '@/lib/utils' -import { INITIAL_RECURRENCE_RULE } from '@/lib/constants' +import { d2s, d2t, getISODate, getNow, parseNaturalLanguageDate, parseNaturalLanguageRRule, parseRRule, serializeRRule } from '@/lib/utils' +import { INITIAL_DUE, INITIAL_RECURRENCE_RULE } from '@/lib/constants' +import * as chrono from 'chrono-node'; +import { DateTime } from 'luxon' interface AddEditHabitModalProps { onClose: () => void @@ -27,12 +29,16 @@ interface AddEditHabitModalProps { export default function AddEditHabitModal({ onClose, onSave, habit }: AddEditHabitModalProps) { const [settings] = useAtom(settingsAtom) + const [browserSettings] = useAtom(browserSettingsAtom) + const isTasksView = browserSettings.viewType === 'tasks' const [name, setName] = useState(habit?.name || '') const [description, setDescription] = useState(habit?.description || '') const [coinReward, setCoinReward] = useState(habit?.coinReward || 1) const [targetCompletions, setTargetCompletions] = useState(habit?.targetCompletions || 1) - const origRuleText = parseRRule(habit?.frequency || INITIAL_RECURRENCE_RULE).toText() + const isRecurRule = !isTasksView + const origRuleText = isRecurRule ? parseRRule(habit?.frequency || INITIAL_RECURRENCE_RULE).toText() : habit?.frequency || INITIAL_DUE const [ruleText, setRuleText] = useState(origRuleText) + const now = getNow({ timezone: settings.system.timezone }) const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() @@ -42,9 +48,8 @@ export default function AddEditHabitModal({ onClose, onSave, habit }: AddEditHab coinReward, targetCompletions: targetCompletions > 1 ? targetCompletions : undefined, completions: habit?.completions || [], - frequency: habit ? ( - origRuleText === ruleText ? habit.frequency : serializeRRule(parseNaturalLanguageRRule(ruleText)) - ) : serializeRRule(parseNaturalLanguageRRule(ruleText)), + frequency: isRecurRule ? serializeRRule(parseNaturalLanguageRRule(ruleText)) : d2t({ dateTime: parseNaturalLanguageDate({ text: ruleText, timezone: settings.system.timezone }) }), + isTask: isTasksView ? true : undefined }) } @@ -52,7 +57,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit }: AddEditHab - {habit ? 'Edit Habit' : 'Add New Habit'} + {habit ? `Edit ${isTasksView ? 'Task' : 'Habit'}` : `Add New ${isTasksView ? 'Task' : 'Habit'}`}
@@ -109,7 +114,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit }: AddEditHab
{(() => { try { - return parseNaturalLanguageRRule(ruleText).toText() + return isRecurRule ? parseNaturalLanguageRRule(ruleText).toText() : d2s({ dateTime: parseNaturalLanguageDate({ text: ruleText, timezone: settings.system.timezone }), timezone: settings.system.timezone, format: DateTime.DATE_MED_WITH_WEEKDAY }) } catch (e: unknown) { return `Invalid rule: ${e instanceof Error ? e.message : 'Invalid recurrence rule'}` } @@ -134,7 +139,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit }: AddEditHab
@@ -168,7 +173,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit }: AddEditHab
- times per occurrence + times
@@ -176,7 +181,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit }: AddEditHab
@@ -207,14 +212,14 @@ export default function AddEditHabitModal({ onClose, onSave, habit }: AddEditHab
- coins per completion + coins
- +
diff --git a/components/DailyOverview.tsx b/components/DailyOverview.tsx index d064251..7658c81 100644 --- a/components/DailyOverview.tsx +++ b/components/DailyOverview.tsx @@ -9,7 +9,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, transientSettingsAtom } from '@/lib/atoms' +import { pomodoroAtom, settingsAtom, completedHabitsMapAtom, browserSettingsAtom } 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' @@ -32,16 +32,21 @@ export default function DailyOverview({ }: UpcomingItemsProps) { const { completeHabit, undoComplete } = useHabits() const [settings] = useAtom(settingsAtom) + const [browserSettings] = useAtom(browserSettingsAtom) const [dailyHabits, setDailyHabits] = useState([]) const [completedHabitsMap] = useAtom(completedHabitsMapAtom) const today = getTodayInTimezone(settings.system.timezone) const todayCompletions = completedHabitsMap.get(today) || [] + const isTasksView = browserSettings.viewType === 'tasks' useEffect(() => { // Filter habits that are due today based on their recurrence rule - const filteredHabits = habits.filter(habit => isHabitDueToday({ habit, timezone: settings.system.timezone })) + const filteredHabits = habits.filter(habit => + (isTasksView ? habit.isTask : !habit.isTask) && + isHabitDueToday({ habit, timezone: settings.system.timezone }) + ) setDailyHabits(filteredHabits) - }, [habits]) + }, [habits, isTasksView]) // Get all wishlist items sorted by redeemable status (non-redeemable first) then by coin cost const sortedWishlistItems = wishlistItems @@ -72,7 +77,7 @@ export default function DailyOverview({
-

Daily Habits

+

{isTasksView ? 'Daily Tasks' : 'Daily Habits'}

{`${dailyHabits.filter(habit => { const completions = (completedHabitsMap.get(today) || []) diff --git a/components/Dashboard.tsx b/components/Dashboard.tsx index 8aaf557..c1d17f4 100644 --- a/components/Dashboard.tsx +++ b/components/Dashboard.tsx @@ -21,7 +21,7 @@ export default function Dashboard() {

Dashboard

- {/* */} +
diff --git a/components/HabitItem.tsx b/components/HabitItem.tsx index c233b7b..d819c64 100644 --- a/components/HabitItem.tsx +++ b/components/HabitItem.tsx @@ -1,7 +1,7 @@ import { Habit } from '@/lib/types' import { useAtom } from 'jotai' -import { settingsAtom, pomodoroAtom } from '@/lib/atoms' -import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, parseNaturalLanguageRRule, parseRRule } from '@/lib/utils' +import { settingsAtom, pomodoroAtom, browserSettingsAtom } from '@/lib/atoms' +import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, parseNaturalLanguageRRule, parseRRule, d2s } from '@/lib/utils' import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card' import { Button } from '@/components/ui/button' import { Coins, Edit, Trash2, Check, Undo2, MoreVertical, Timer } from 'lucide-react' @@ -15,6 +15,7 @@ import { import { useEffect, useState } from 'react' import { useHabits } from '@/hooks/useHabits' import { INITIAL_RECURRENCE_RULE } from '@/lib/constants' +import { DateTime } from 'luxon' interface HabitItemProps { habit: Habit @@ -32,6 +33,9 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) { const target = habit.targetCompletions || 1 const isCompletedToday = completionsToday >= target const [isHighlighted, setIsHighlighted] = useState(false) + const [browserSettings] = useAtom(browserSettingsAtom) + const isTasksView = browserSettings.viewType === 'tasks' + const isRecurRule = !isTasksView useEffect(() => { const params = new URLSearchParams(window.location.search) @@ -66,7 +70,7 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) { )} -

Frequency: {parseRRule(habit.frequency || INITIAL_RECURRENCE_RULE).toText()}

+

When: {isRecurRule ? parseRRule(habit.frequency || INITIAL_RECURRENCE_RULE).toText() : d2s({ dateTime: t2d({ timestamp: habit.frequency, timezone: settings.system.timezone }), timezone: settings.system.timezone, format: DateTime.DATE_MED_WITH_WEEKDAY })}

{habit.coinReward} coins per completion diff --git a/components/HabitList.tsx b/components/HabitList.tsx index 9bbfb4c..89d43bc 100644 --- a/components/HabitList.tsx +++ b/components/HabitList.tsx @@ -3,7 +3,7 @@ import { useState } from 'react' import { Plus, ListTodo } from 'lucide-react' import { useAtom } from 'jotai' -import { habitsAtom, settingsAtom } from '@/lib/atoms' +import { habitsAtom, settingsAtom, browserSettingsAtom } from '@/lib/atoms' import EmptyState from './EmptyState' import { Button } from '@/components/ui/button' import HabitItem from './HabitItem' @@ -11,11 +11,16 @@ import AddEditHabitModal from './AddEditHabitModal' import ConfirmDialog from './ConfirmDialog' import { Habit } from '@/lib/types' import { useHabits } from '@/hooks/useHabits' +import { HabitIcon, TaskIcon } from '@/lib/constants' export default function HabitList() { const { saveHabit, deleteHabit } = useHabits() const [habitsData, setHabitsData] = useAtom(habitsAtom) - const habits = habitsData.habits + const [browserSettings] = useAtom(browserSettingsAtom) + const isTasksView = browserSettings.viewType === 'tasks' + const habits = habitsData.habits.filter(habit => + isTasksView ? habit.isTask : !habit.isTask + ) const [settings] = useAtom(settingsAtom) const [isModalOpen, setIsModalOpen] = useState(false) const [editingHabit, setEditingHabit] = useState(null) @@ -28,18 +33,20 @@ export default function HabitList() { return (
-

My Habits

+

+ {isTasksView ? 'My Tasks' : 'My Habits'} +

{habits.length === 0 ? (
) : ( @@ -79,8 +86,8 @@ export default function HabitList() { } setDeleteConfirmation({ isOpen: false, habitId: null }) }} - title="Delete Habit" - message="Are you sure you want to delete this habit? This action cannot be undone." + title={isTasksView ? "Delete Task" : "Delete Habit"} + message={isTasksView ? "Are you sure you want to delete this task? This action cannot be undone." : "Are you sure you want to delete this habit? This action cannot be undone."} confirmText="Delete" />
diff --git a/components/Header.tsx b/components/Header.tsx index 3d92e19..db4a22e 100644 --- a/components/Header.tsx +++ b/components/Header.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { useAtom } from 'jotai' -import { coinsAtom, settingsAtom } from '@/lib/atoms' +import { coinsAtom, settingsAtom, browserSettingsAtom } from '@/lib/atoms' import { useCoins } from '@/hooks/useCoins' import { FormattedNumber } from '@/components/FormattedNumber' import { Bell, Menu, Settings, User, Info, Coins } from 'lucide-react' @@ -29,6 +29,8 @@ export default function Header({ className }: HeaderProps) { const [showAbout, setShowAbout] = useState(false) const [settings] = useAtom(settingsAtom) const [coins] = useAtom(coinsAtom) + const [browserSettings] = useAtom(browserSettingsAtom) + const isTasksView = browserSettings.viewType === 'tasks' return ( <>
diff --git a/components/Navigation.tsx b/components/Navigation.tsx index c22e2c6..4bcc6b3 100644 --- a/components/Navigation.tsx +++ b/components/Navigation.tsx @@ -1,15 +1,23 @@ 'use client' import Link from 'next/link' -import { Home, Calendar, List, Gift, Coins, Settings, Info } from 'lucide-react' +import { Home, Calendar, List, Gift, Coins, Settings, Info, CheckSquare } from 'lucide-react' +import { useAtom } from 'jotai' +import { browserSettingsAtom } from '@/lib/atoms' import { useEffect, useState } from 'react' import AboutModal from './AboutModal' +import { HabitIcon, TaskIcon } from '@/lib/constants' type ViewPort = 'main' | 'mobile' -const navItems = [ +const navItems = (isTasksView: boolean) => [ { icon: Home, label: 'Dashboard', href: '/', position: 'main' }, - { icon: List, label: 'Habits', href: '/habits', position: 'main' }, + { + icon: isTasksView ? TaskIcon : HabitIcon, + label: isTasksView ? 'Tasks' : 'Habits', + href: '/habits', + position: 'main' + }, { icon: Calendar, label: 'Calendar', href: '/calendar', position: 'main' }, { icon: Gift, label: 'Wishlist', href: '/wishlist', position: 'main' }, { icon: Coins, label: 'Coins', href: '/coins', position: 'main' }, @@ -23,6 +31,8 @@ interface NavigationProps { export default function Navigation({ className, viewPort }: NavigationProps) { const [showAbout, setShowAbout] = useState(false) const [isMobileView, setIsMobileView] = useState(false) + const [browserSettings] = useAtom(browserSettingsAtom) + const isTasksView = browserSettings.viewType === 'tasks' useEffect(() => { const handleResize = () => { @@ -45,7 +55,7 @@ export default function Navigation({ className, viewPort }: NavigationProps) {
{/* Add padding at the bottom to prevent content from being hidden */}