Files
HabitTrove/hooks/useHabits.tsx

298 lines
9.8 KiB
TypeScript

import { addCoins, removeCoins, saveHabitsData } from '@/app/actions/data'
import { ToastAction } from '@/components/ui/toast'
import { toast } from '@/hooks/use-toast'
import { coinsAtom, currentUserAtom, habitFreqMapAtom, habitsAtom, settingsAtom, usersAtom } from '@/lib/atoms'
import { Habit } from '@/lib/types'
import {
d2s,
d2t,
getCompletionsForDate,
getISODate,
getNow,
getTodayInTimezone,
handlePermissionCheck,
isSameDate,
playSound,
t2d
} from '@/lib/utils'
import { useAtom } from 'jotai'
import { Undo2 } from 'lucide-react'
import { DateTime } from 'luxon'
import { useTranslations } from 'next-intl'
export function useHabits() {
const t = useTranslations('useHabits');
const tCommon = useTranslations('Common');
const [usersData] = useAtom(usersAtom)
const [currentUser] = useAtom(currentUserAtom)
const [habitsData, setHabitsData] = useAtom(habitsAtom)
const [coins, setCoins] = useAtom(coinsAtom)
const [settings] = useAtom(settingsAtom)
const [habitFreqMap] = useAtom(habitFreqMapAtom)
const completeHabit = async (habit: Habit) => {
if (!handlePermissionCheck(currentUser, 'habit', 'interact', tCommon)) return
const timezone = settings.system.timezone
const today = getTodayInTimezone(timezone)
// Get current completions for today
const completionsToday = getCompletionsForDate({
habit,
date: today,
timezone
})
const target = habit.targetCompletions || 1
// Check if already completed
if (completionsToday >= target) {
toast({
title: t("alreadyCompletedTitle"),
description: t("alreadyCompletedDescription"),
variant: "destructive",
})
return
}
// Add new completion
const updatedHabit = {
...habit,
completions: [...habit.completions, d2t({ dateTime: getNow({ timezone }) })],
// Archive the habit if it's a task and we're about to reach the target
archived: habit.isTask && completionsToday + 1 === target ? true : habit.archived
}
const updatedHabits = habitsData.habits.map(h =>
h.id === habit.id ? updatedHabit : h
)
await saveHabitsData({ habits: updatedHabits })
// Check if we've now reached the target
const isTargetReached = completionsToday + 1 === target
if (isTargetReached) {
const updatedCoins = await addCoins({
amount: habit.coinReward,
description: `Completed: ${habit.name}`,
type: habit.isTask ? 'TASK_COMPLETION' : 'HABIT_COMPLETION',
relatedItemId: habit.id,
})
playSound()
toast({
title: t("completedTitle"),
description: t("earnedCoinsDescription", { coinReward: habit.coinReward }),
action: <ToastAction altText={tCommon('undoButton')} className="gap-2" onClick={() => undoComplete(updatedHabit)}>
<Undo2 className="h-4 w-4" />{tCommon('undoButton')}
</ToastAction>
})
setCoins(updatedCoins)
} else {
toast({
title: t("progressTitle"),
description: t("progressDescription", { count: completionsToday + 1, target }),
action: <ToastAction altText={tCommon('undoButton')} className="gap-2" onClick={() => undoComplete(updatedHabit)}>
<Undo2 className="h-4 w-4" />{tCommon('undoButton')}
</ToastAction>
})
}
// move atom update at the end of function to improve UI responsiveness
setHabitsData({ habits: updatedHabits })
return {
updatedHabits,
newBalance: coins.balance,
newTransactions: coins.transactions
}
}
const undoComplete = async (habit: Habit) => {
if (!handlePermissionCheck(currentUser, 'habit', 'interact', tCommon)) return
const timezone = settings.system.timezone
const today = t2d({ timestamp: getTodayInTimezone(timezone), timezone })
// Get today's completions
const todayCompletions = habit.completions.filter(completion =>
isSameDate(t2d({ timestamp: completion, timezone }), today)
)
if (todayCompletions.length > 0) {
// Remove the most recent completion and unarchive if needed
const updatedHabit = {
...habit,
completions: habit.completions.filter(
(_, index) => index !== habit.completions.length - 1
),
archived: habit.isTask ? false : habit.archived // Unarchive if it's a task
}
const updatedHabits = habitsData.habits.map(h =>
h.id === habit.id ? updatedHabit : h
)
await saveHabitsData({ habits: updatedHabits })
setHabitsData({ habits: updatedHabits })
// If we were at the target, remove the coins
const target = habit.targetCompletions || 1
if (todayCompletions.length === target) {
const updatedCoins = await removeCoins({
amount: habit.coinReward,
description: `Undid completion: ${habit.name}`,
type: habit.isTask ? 'TASK_UNDO' : 'HABIT_UNDO',
relatedItemId: habit.id,
})
setCoins(updatedCoins)
}
toast({
title: t("completionUndoneTitle"),
description: t("completionUndoneDescription", {
count: getCompletionsForDate({
habit: updatedHabit,
date: today,
timezone
}),
target
}),
action: <ToastAction altText={tCommon('redoButton')} onClick={() => completeHabit(updatedHabit)}>
<Undo2 className="h-4 w-4" />{tCommon('redoButton')}
</ToastAction>
})
return {
updatedHabits,
newBalance: coins.balance,
newTransactions: coins.transactions
}
} else {
toast({
title: t("noCompletionsToUndoTitle"),
description: t("noCompletionsToUndoDescription"),
variant: "destructive",
})
return
}
}
const saveHabit = async (habit: Omit<Habit, 'id'> & { id?: string }) => {
if (!handlePermissionCheck(currentUser, 'habit', 'write', tCommon)) return
const newHabit = {
...habit,
id: habit.id || crypto.randomUUID()
}
const updatedHabits = habit.id
? habitsData.habits.map(h => h.id === habit.id ? newHabit : h)
: [...habitsData.habits, newHabit]
await saveHabitsData({ habits: updatedHabits })
setHabitsData({ habits: updatedHabits })
return updatedHabits
}
const deleteHabit = async (id: string) => {
if (!handlePermissionCheck(currentUser, 'habit', 'write', tCommon)) return
const updatedHabits = habitsData.habits.filter(h => h.id !== id)
await saveHabitsData({ habits: updatedHabits })
setHabitsData({ habits: updatedHabits })
return updatedHabits
}
const completePastHabit = async (habit: Habit, date: DateTime) => {
if (!handlePermissionCheck(currentUser, 'habit', 'interact', tCommon)) return
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: t("alreadyCompletedPastDateTitle"),
description: t("alreadyCompletedPastDateDescription", { dateKey: d2s({ dateTime: date, timezone, format: 'yyyy-MM-dd' }) }),
variant: "destructive",
})
return
}
// 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({
amount: habit.coinReward,
description: `Completed: ${habit.name} on ${d2s({ dateTime: date, timezone, format: 'yyyy-MM-dd' })}`,
type: habit.isTask ? 'TASK_COMPLETION' : 'HABIT_COMPLETION',
relatedItemId: habit.id,
})
setCoins(updatedCoins)
}
toast({
title: isTargetReached ? t("completedTitle") : t("progressTitle"),
description: isTargetReached
? t("earnedCoinsPastDateDescription", { coinReward: habit.coinReward, dateKey })
: t("progressPastDateDescription", { count: completionsOnDate + 1, target, dateKey }),
action: <ToastAction altText={tCommon('undoButton')} className="gap-2" onClick={() => undoComplete(updatedHabit)}>
<Undo2 className="h-4 w-4" />{tCommon('undoButton')}
</ToastAction>
})
return {
updatedHabits,
newBalance: coins.balance,
newTransactions: coins.transactions
}
}
const archiveHabit = async (id: string) => {
if (!handlePermissionCheck(currentUser, 'habit', 'write', tCommon)) return
const updatedHabits = habitsData.habits.map(h =>
h.id === id ? { ...h, archived: true } : h
)
await saveHabitsData({ habits: updatedHabits })
setHabitsData({ habits: updatedHabits })
}
const unarchiveHabit = async (id: string) => {
if (!handlePermissionCheck(currentUser, 'habit', 'write', tCommon)) return
const updatedHabits = habitsData.habits.map(h =>
h.id === id ? { ...h, archived: false } : h
)
await saveHabitsData({ habits: updatedHabits })
setHabitsData({ habits: updatedHabits })
}
return {
completeHabit,
undoComplete,
saveHabit,
deleteHabit,
completePastHabit,
archiveHabit,
unarchiveHabit,
habitFreqMap,
}
}