mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
Multiuser support (#60)
This commit is contained in:
@@ -1,25 +1,57 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { checkPermission } from '@/lib/utils'
|
||||
import {
|
||||
coinsAtom,
|
||||
coinsEarnedTodayAtom,
|
||||
totalEarnedAtom,
|
||||
totalSpentAtom,
|
||||
coinsSpentTodayAtom,
|
||||
transactionsTodayAtom
|
||||
transactionsTodayAtom,
|
||||
coinsBalanceAtom
|
||||
} from '@/lib/atoms'
|
||||
import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data'
|
||||
import { CoinsData } from '@/lib/types'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
|
||||
function handlePermissionCheck(
|
||||
user: any,
|
||||
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 useCoins() {
|
||||
const { currentUser: user } = useHelpers()
|
||||
const [coins, setCoins] = useAtom(coinsAtom)
|
||||
const [coinsEarnedToday] = useAtom(coinsEarnedTodayAtom)
|
||||
const [totalEarned] = useAtom(totalEarnedAtom)
|
||||
const [totalSpent] = useAtom(totalSpentAtom)
|
||||
const [coinsSpentToday] = useAtom(coinsSpentTodayAtom)
|
||||
const [transactionsToday] = useAtom(transactionsTodayAtom)
|
||||
const [balance] = useAtom(coinsBalanceAtom)
|
||||
|
||||
const add = async (amount: number, description: string, note?: string) => {
|
||||
if (!handlePermissionCheck(user, 'coins', 'write')) return null
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
toast({
|
||||
title: "Invalid amount",
|
||||
@@ -40,6 +72,7 @@ export function useCoins() {
|
||||
}
|
||||
|
||||
const remove = async (amount: number, description: string, note?: string) => {
|
||||
if (!handlePermissionCheck(user, 'coins', 'write')) return null
|
||||
const numAmount = Math.abs(amount)
|
||||
if (isNaN(numAmount) || numAmount <= 0) {
|
||||
toast({
|
||||
@@ -61,6 +94,7 @@ export function useCoins() {
|
||||
}
|
||||
|
||||
const updateNote = async (transactionId: string, note: string) => {
|
||||
if (!handlePermissionCheck(user, 'coins', 'write')) return null
|
||||
const transaction = coins.transactions.find(t => t.id === transactionId)
|
||||
if (!transaction) {
|
||||
toast({
|
||||
@@ -93,7 +127,7 @@ export function useCoins() {
|
||||
add,
|
||||
remove,
|
||||
updateNote,
|
||||
balance: coins.balance,
|
||||
balance,
|
||||
transactions: coins.transactions,
|
||||
coinsEarnedToday,
|
||||
totalEarned,
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -1,37 +1,73 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { wishlistAtom, coinsAtom } from '@/lib/atoms'
|
||||
import { wishlistAtom, coinsAtom, coinsBalanceAtom } from '@/lib/atoms'
|
||||
import { saveWishlistItems, removeCoins } from '@/app/actions/data'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { WishlistItemType } from '@/lib/types'
|
||||
import { celebrations } from '@/utils/celebrations'
|
||||
import { checkPermission } from '@/lib/utils'
|
||||
import { useHelpers } from '@/lib/client-helpers'
|
||||
|
||||
function handlePermissionCheck(
|
||||
user: any,
|
||||
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 useWishlist() {
|
||||
const { currentUser: user } = useHelpers()
|
||||
const [wishlist, setWishlist] = useAtom(wishlistAtom)
|
||||
const [coins, setCoins] = useAtom(coinsAtom)
|
||||
const balance = coins.balance
|
||||
const [balance] = useAtom(coinsBalanceAtom)
|
||||
|
||||
const addWishlistItem = async (item: Omit<WishlistItemType, 'id'>) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
const newItem = { ...item, id: Date.now().toString() }
|
||||
const newItems = [...wishlist.items, newItem]
|
||||
setWishlist({ items: newItems })
|
||||
await saveWishlistItems(newItems)
|
||||
const newWishListData = { items: newItems }
|
||||
setWishlist(newWishListData)
|
||||
await saveWishlistItems(newWishListData)
|
||||
}
|
||||
|
||||
const editWishlistItem = async (updatedItem: WishlistItemType) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
const newItems = wishlist.items.map(item =>
|
||||
item.id === updatedItem.id ? updatedItem : item
|
||||
)
|
||||
setWishlist({ items: newItems })
|
||||
await saveWishlistItems(newItems)
|
||||
const newWishListData = { items: newItems }
|
||||
setWishlist(newWishListData)
|
||||
await saveWishlistItems(newWishListData)
|
||||
}
|
||||
|
||||
const deleteWishlistItem = async (id: string) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
const newItems = wishlist.items.filter(item => item.id !== id)
|
||||
setWishlist({ items: newItems })
|
||||
await saveWishlistItems(newItems)
|
||||
const newWishListData = { items: newItems }
|
||||
setWishlist(newWishListData)
|
||||
await saveWishlistItems(newWishListData)
|
||||
}
|
||||
|
||||
const redeemWishlistItem = async (item: WishlistItemType) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'interact')) return false
|
||||
if (balance >= item.coinCost) {
|
||||
// Check if item has target completions and if we've reached the limit
|
||||
if (item.targetCompletions && item.targetCompletions <= 0) {
|
||||
@@ -71,8 +107,9 @@ export function useWishlist() {
|
||||
}
|
||||
return wishlistItem
|
||||
})
|
||||
setWishlist({ items: newItems })
|
||||
await saveWishlistItems(newItems)
|
||||
const newWishListData = { items: newItems }
|
||||
setWishlist(newWishListData)
|
||||
await saveWishlistItems(newWishListData)
|
||||
}
|
||||
|
||||
// Randomly choose a celebration effect
|
||||
@@ -101,19 +138,23 @@ export function useWishlist() {
|
||||
const canRedeem = (cost: number) => balance >= cost
|
||||
|
||||
const archiveWishlistItem = async (id: string) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
const newItems = wishlist.items.map(item =>
|
||||
item.id === id ? { ...item, archived: true } : item
|
||||
)
|
||||
setWishlist({ items: newItems })
|
||||
await saveWishlistItems(newItems)
|
||||
const newWishListData = { items: newItems }
|
||||
setWishlist(newWishListData)
|
||||
await saveWishlistItems(newWishListData)
|
||||
}
|
||||
|
||||
const unarchiveWishlistItem = async (id: string) => {
|
||||
if (!handlePermissionCheck(user, 'wishlist', 'write')) return
|
||||
const newItems = wishlist.items.map(item =>
|
||||
item.id === id ? { ...item, archived: undefined } : item
|
||||
item.id === id ? { ...item, archived: false } : item
|
||||
)
|
||||
setWishlist({ items: newItems })
|
||||
await saveWishlistItems(newItems)
|
||||
const newWishListData = { items: newItems }
|
||||
setWishlist(newWishListData)
|
||||
await saveWishlistItems(newWishListData)
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user