Multiuser support (#60)

This commit is contained in:
Doh
2025-02-18 23:43:23 -05:00
committed by GitHub
parent 363b31e934
commit 8ac2ec053d
48 changed files with 2678 additions and 271 deletions

View File

@@ -1,30 +1,62 @@
import { useAtom } from 'jotai'
import { habitsAtom, coinsAtom, settingsAtom } from '@/lib/atoms'
import { habitsAtom, coinsAtom, settingsAtom, usersAtom } from '@/lib/atoms'
import { addCoins, removeCoins, saveHabitsData } from '@/app/actions/data'
import { Habit } from '@/lib/types'
import { DateTime } from 'luxon'
import {
getNowInMilliseconds,
getTodayInTimezone,
isSameDate,
t2d,
d2t,
getNow,
getCompletionsForDate,
getISODate,
d2s,
playSound
} from '@/lib/utils'
import { Habit, Permission, SafeUser, User } from '@/lib/types'
import { toast } from '@/hooks/use-toast'
import { DateTime } from 'luxon'
import {
getNowInMilliseconds,
getTodayInTimezone,
isSameDate,
t2d,
d2t,
getNow,
getCompletionsForDate,
getISODate,
d2s,
playSound,
checkPermission
} from '@/lib/utils'
import { ToastAction } from '@/components/ui/toast'
import { Undo2 } from 'lucide-react'
import { useHelpers } from '@/lib/client-helpers'
function handlePermissionCheck(
user: SafeUser | undefined,
resource: 'habit' | 'wishlist' | 'coins',
action: 'write' | 'interact'
): boolean {
if (!user) {
toast({
title: "Authentication Required",
description: "Please sign in to continue.",
variant: "destructive",
})
return false
}
if (!user.isAdmin && !checkPermission(user.permissions, resource, action)) {
toast({
title: "Permission Denied",
description: `You don't have ${action} permission for ${resource}s.`,
variant: "destructive",
})
return false
}
return true
}
export function useHabits() {
const [usersData] = useAtom(usersAtom)
const { currentUser } = useHelpers()
const [habitsData, setHabitsData] = useAtom(habitsAtom)
const [coins, setCoins] = useAtom(coinsAtom)
const [settings] = useAtom(settingsAtom)
const completeHabit = async (habit: Habit) => {
if (!handlePermissionCheck(currentUser, 'habit', 'interact')) return
const timezone = settings.system.timezone
const today = getTodayInTimezone(timezone)
@@ -43,7 +75,7 @@ export function useHabits() {
description: `You've already completed this habit today.`,
variant: "destructive",
})
return null
return
}
// Add new completion
@@ -71,7 +103,7 @@ export function useHabits() {
})
isTargetReached && playSound()
toast({
title: "Habit completed!",
title: "Completed!",
description: `You earned ${habit.coinReward} coins.`,
action: <ToastAction altText="Undo" className="gap-2" onClick={() => undoComplete(updatedHabit)}>
<Undo2 className="h-4 w-4" />Undo
@@ -98,6 +130,7 @@ export function useHabits() {
}
const undoComplete = async (habit: Habit) => {
if (!handlePermissionCheck(currentUser, 'habit', 'interact')) return
const timezone = settings.system.timezone
const today = t2d({ timestamp: getTodayInTimezone(timezone), timezone })
@@ -113,7 +146,7 @@ export function useHabits() {
completions: habit.completions.filter(
(_, index) => index !== habit.completions.length - 1
),
archived: habit.isTask ? undefined : habit.archived // Unarchive if it's a task
archived: habit.isTask ? false : habit.archived // Unarchive if it's a task
}
const updatedHabits = habitsData.habits.map(h =>
@@ -158,11 +191,12 @@ export function useHabits() {
description: "This habit hasn't been completed today.",
variant: "destructive",
})
return null
return
}
}
const saveHabit = async (habit: Omit<Habit, 'id'> & { id?: string }) => {
if (!handlePermissionCheck(currentUser, 'habit', 'write')) return
const newHabit = {
...habit,
id: habit.id || getNowInMilliseconds().toString()
@@ -177,6 +211,7 @@ export function useHabits() {
}
const deleteHabit = async (id: string) => {
if (!handlePermissionCheck(currentUser, 'habit', 'write')) return
const updatedHabits = habitsData.habits.filter(h => h.id !== id)
await saveHabitsData({ habits: updatedHabits })
setHabitsData({ habits: updatedHabits })
@@ -184,6 +219,7 @@ export function useHabits() {
}
const completePastHabit = async (habit: Habit, date: DateTime) => {
if (!handlePermissionCheck(currentUser, 'habit', 'interact')) return
const timezone = settings.system.timezone
const dateKey = getISODate({ dateTime: date, timezone })
@@ -199,7 +235,7 @@ export function useHabits() {
description: `This habit was already completed on ${d2s({ dateTime: date, timezone, format: 'yyyy-MM-dd' })}.`,
variant: "destructive",
})
return null
return
}
// Use current time but with the past date
@@ -236,7 +272,7 @@ export function useHabits() {
}
toast({
title: isTargetReached ? "Habit completed!" : "Progress!",
title: isTargetReached ? "Completed!" : "Progress!",
description: isTargetReached
? `You earned ${habit.coinReward} coins for ${dateKey}.`
: `You've completed ${completionsOnDate + 1}/${target} times on ${dateKey}.`,
@@ -253,6 +289,7 @@ export function useHabits() {
}
const archiveHabit = async (id: string) => {
if (!handlePermissionCheck(currentUser, 'habit', 'write')) return
const updatedHabits = habitsData.habits.map(h =>
h.id === id ? { ...h, archived: true } : h
)
@@ -261,8 +298,9 @@ export function useHabits() {
}
const unarchiveHabit = async (id: string) => {
if (!handlePermissionCheck(currentUser, 'habit', 'write')) return
const updatedHabits = habitsData.habits.map(h =>
h.id === id ? { ...h, archived: undefined } : h
h.id === id ? { ...h, archived: false } : h
)
await saveHabitsData({ habits: updatedHabits })
setHabitsData({ habits: updatedHabits })