mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-03-08 03:29:49 +01:00
fix coin balance
This commit is contained in:
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Version 0.2.20
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
* coin balance shows correct value for selected user in coin management view (#151)
|
||||||
|
|
||||||
|
### Improved
|
||||||
|
|
||||||
|
* refactor code to remove client-helpers hook
|
||||||
|
|
||||||
## Version 0.2.19
|
## Version 0.2.19
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from 'react'
|
|||||||
import { RRule, RRuleSet, rrulestr } from 'rrule'
|
import { RRule, RRuleSet, rrulestr } from 'rrule'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { settingsAtom, browserSettingsAtom, usersAtom } from '@/lib/atoms'
|
import { settingsAtom, browserSettingsAtom, usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
||||||
@@ -18,7 +18,7 @@ import EmojiPickerButton from './EmojiPickerButton'
|
|||||||
import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2s, d2t, serializeRRule } from '@/lib/utils'
|
import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2s, d2t, serializeRRule } from '@/lib/utils'
|
||||||
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES, RECURRENCE_RULE_MAP, MAX_COIN_LIMIT } from '@/lib/constants'
|
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, QUICK_DATES, RECURRENCE_RULE_MAP, MAX_COIN_LIMIT } from '@/lib/constants'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
|
|
||||||
interface AddEditHabitModalProps {
|
interface AddEditHabitModalProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
@@ -42,7 +42,7 @@ export default function AddEditHabitModal({ onClose, onSave, habit, isTask }: Ad
|
|||||||
timezone: settings.system.timezone
|
timezone: settings.system.timezone
|
||||||
}) : (isRecurRule ? INITIAL_RECURRENCE_RULE : INITIAL_DUE);
|
}) : (isRecurRule ? INITIAL_RECURRENCE_RULE : INITIAL_DUE);
|
||||||
const [ruleText, setRuleText] = useState<string>(initialRuleText)
|
const [ruleText, setRuleText] = useState<string>(initialRuleText)
|
||||||
const { currentUser } = useHelpers()
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
const [isQuickDatesOpen, setIsQuickDatesOpen] = useState(false)
|
const [isQuickDatesOpen, setIsQuickDatesOpen] = useState(false)
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null); // State for validation message
|
const [errorMessage, setErrorMessage] = useState<string | null>(null); // State for validation message
|
||||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>((habit?.userIds || []).filter(id => id !== currentUser?.id))
|
const [selectedUserIds, setSelectedUserIds] = useState<string[]>((habit?.userIds || []).filter(id => id !== currentUser?.id))
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { usersAtom } from '@/lib/atoms'
|
import { usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -36,7 +35,7 @@ export default function AddEditWishlistItemModal({
|
|||||||
const [coinCost, setCoinCost] = useState(editingItem?.coinCost || 1)
|
const [coinCost, setCoinCost] = useState(editingItem?.coinCost || 1)
|
||||||
const [targetCompletions, setTargetCompletions] = useState<number | undefined>(editingItem?.targetCompletions)
|
const [targetCompletions, setTargetCompletions] = useState<number | undefined>(editingItem?.targetCompletions)
|
||||||
const [link, setLink] = useState(editingItem?.link || '')
|
const [link, setLink] = useState(editingItem?.link || '')
|
||||||
const { currentUser } = useHelpers()
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>((editingItem?.userIds || []).filter(id => id !== currentUser?.id))
|
const [selectedUserIds, setSelectedUserIds] = useState<string[]>((editingItem?.userIds || []).filter(id => id !== currentUser?.id))
|
||||||
const [errors, setErrors] = useState<{ [key: string]: string }>({})
|
const [errors, setErrors] = useState<{ [key: string]: string }>({})
|
||||||
const [usersData] = useAtom(usersAtom)
|
const [usersData] = useAtom(usersAtom)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { ReactNode, Suspense, useEffect, useState } from 'react'
|
import { ReactNode, Suspense, useEffect, useState } from 'react'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom, useSetAtom } from 'jotai' // Import useSetAtom
|
||||||
import { aboutOpenAtom, pomodoroAtom, userSelectAtom } from '@/lib/atoms'
|
import { aboutOpenAtom, pomodoroAtom, userSelectAtom, currentUserIdAtom } from '@/lib/atoms' // Import currentUserIdAtom
|
||||||
import PomodoroTimer from './PomodoroTimer'
|
import PomodoroTimer from './PomodoroTimer'
|
||||||
import UserSelectModal from './UserSelectModal'
|
import UserSelectModal from './UserSelectModal'
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
@@ -13,6 +13,7 @@ export default function ClientWrapper({ children }: { children: ReactNode }) {
|
|||||||
const [pomo] = useAtom(pomodoroAtom)
|
const [pomo] = useAtom(pomodoroAtom)
|
||||||
const [userSelect, setUserSelect] = useAtom(userSelectAtom)
|
const [userSelect, setUserSelect] = useAtom(userSelectAtom)
|
||||||
const [aboutOpen, setAboutOpen] = useAtom(aboutOpenAtom)
|
const [aboutOpen, setAboutOpen] = useAtom(aboutOpenAtom)
|
||||||
|
const setCurrentUserIdAtom = useSetAtom(currentUserIdAtom)
|
||||||
const { data: session, status } = useSession()
|
const { data: session, status } = useSession()
|
||||||
const currentUserId = session?.user.id
|
const currentUserId = session?.user.id
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
@@ -27,7 +28,11 @@ export default function ClientWrapper({ children }: { children: ReactNode }) {
|
|||||||
if (!currentUserId && !userSelect) {
|
if (!currentUserId && !userSelect) {
|
||||||
setUserSelect(true)
|
setUserSelect(true)
|
||||||
}
|
}
|
||||||
}, [currentUserId, status, userSelect])
|
}, [currentUserId, status, userSelect, setUserSelect])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentUserIdAtom(currentUserId)
|
||||||
|
}, [currentUserId, setCurrentUserIdAtom])
|
||||||
|
|
||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
return <LoadingSpinner />
|
return <LoadingSpinner />
|
||||||
|
|||||||
@@ -10,19 +10,18 @@ import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
|
|||||||
import EmptyState from './EmptyState'
|
import EmptyState from './EmptyState'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { settingsAtom, usersAtom } from '@/lib/atoms'
|
import { settingsAtom, usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { useCoins } from '@/hooks/useCoins'
|
import { useCoins } from '@/hooks/useCoins'
|
||||||
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
||||||
import { TransactionNoteEditor } from './TransactionNoteEditor'
|
import { TransactionNoteEditor } from './TransactionNoteEditor'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import { TransactionType } from '@/lib/types'
|
import { TransactionType } from '@/lib/types'
|
||||||
|
|
||||||
export default function CoinsManager() {
|
export default function CoinsManager() {
|
||||||
const t = useTranslations('CoinsManager')
|
const t = useTranslations('CoinsManager')
|
||||||
const { currentUser } = useHelpers()
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
const [selectedUser, setSelectedUser] = useState<string>()
|
const [selectedUser, setSelectedUser] = useState<string>()
|
||||||
const {
|
const {
|
||||||
add,
|
add,
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Habit } from '@/lib/types';
|
import { Habit, User } from '@/lib/types';
|
||||||
import { useHabits } from '@/hooks/useHabits';
|
import { useHabits } from '@/hooks/useHabits';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { pomodoroAtom, settingsAtom } from '@/lib/atoms';
|
import { pomodoroAtom, settingsAtom, currentUserAtom } from '@/lib/atoms';
|
||||||
import { d2t, getNow, isHabitDueToday } from '@/lib/utils';
|
import { d2t, getNow, isHabitDueToday, hasPermission } from '@/lib/utils';
|
||||||
import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
|
import { DropdownMenuItem, DropdownMenuSeparator } from '@/components/ui/dropdown-menu';
|
||||||
import { ContextMenuItem, ContextMenuSeparator } from '@/components/ui/context-menu';
|
import { ContextMenuItem, ContextMenuSeparator } from '@/components/ui/context-menu';
|
||||||
import { Timer, Calendar, Pin, Edit, Archive, ArchiveRestore, Trash2 } from 'lucide-react';
|
import { Timer, Calendar, Pin, Edit, Archive, ArchiveRestore, Trash2 } from 'lucide-react';
|
||||||
import { useHelpers } from '@/lib/client-helpers'; // For permission checks if needed, though useHabits handles most
|
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
interface HabitContextMenuItemsProps {
|
interface HabitContextMenuItemsProps {
|
||||||
@@ -28,10 +27,10 @@ export function HabitContextMenuItems({
|
|||||||
const { saveHabit, archiveHabit, unarchiveHabit } = useHabits();
|
const { saveHabit, archiveHabit, unarchiveHabit } = useHabits();
|
||||||
const [settings] = useAtom(settingsAtom);
|
const [settings] = useAtom(settingsAtom);
|
||||||
const [, setPomo] = useAtom(pomodoroAtom);
|
const [, setPomo] = useAtom(pomodoroAtom);
|
||||||
const { hasPermission } = useHelpers(); // Assuming useHabits handles permissions for its actions
|
const [currentUser] = useAtom(currentUserAtom);
|
||||||
|
|
||||||
const canWrite = hasPermission('habit', 'write'); // For UI disabling if not handled by useHabits' actions
|
const canWrite = hasPermission(currentUser, 'habit', 'write'); // For UI disabling if not handled by useHabits' actions
|
||||||
const canInteract = hasPermission('habit', 'interact');
|
const canInteract = hasPermission(currentUser, 'habit', 'interact');
|
||||||
|
|
||||||
const MenuItemComponent = context === 'daily-overview' ? ContextMenuItem : DropdownMenuItem;
|
const MenuItemComponent = context === 'daily-overview' ? ContextMenuItem : DropdownMenuItem;
|
||||||
const MenuSeparatorComponent = context === 'daily-overview' ? ContextMenuSeparator : DropdownMenuSeparator;
|
const MenuSeparatorComponent = context === 'daily-overview' ? ContextMenuSeparator : DropdownMenuSeparator;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { Habit, SafeUser, User, Permission } from '@/lib/types'
|
import { Habit, SafeUser, User, Permission } from '@/lib/types'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { settingsAtom, pomodoroAtom, browserSettingsAtom, usersAtom } from '@/lib/atoms'
|
import { settingsAtom, pomodoroAtom, browserSettingsAtom, usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||||
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, d2s, getCompletionsForToday, isTaskOverdue, convertMachineReadableFrequencyToHumanReadable } from '@/lib/utils'
|
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, d2s, getCompletionsForToday, isTaskOverdue, convertMachineReadableFrequencyToHumanReadable } from '@/lib/utils'
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Coins, Edit, Check, Undo2, MoreVertical, Pin } from 'lucide-react' // Removed unused icons
|
import { Coins, Edit, Check, Undo2, MoreVertical, Pin } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -18,7 +18,7 @@ import { useTranslations } from 'next-intl'
|
|||||||
import { INITIAL_RECURRENCE_RULE, RECURRENCE_RULE_MAP } from '@/lib/constants'
|
import { INITIAL_RECURRENCE_RULE, RECURRENCE_RULE_MAP } from '@/lib/constants'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
import { hasPermission } from '@/lib/utils'
|
||||||
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
||||||
|
|
||||||
interface HabitItemProps {
|
interface HabitItemProps {
|
||||||
@@ -57,9 +57,9 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
|||||||
const [isHighlighted, setIsHighlighted] = useState(false)
|
const [isHighlighted, setIsHighlighted] = useState(false)
|
||||||
const t = useTranslations('HabitItem');
|
const t = useTranslations('HabitItem');
|
||||||
const [usersData] = useAtom(usersAtom)
|
const [usersData] = useAtom(usersAtom)
|
||||||
const { currentUser, hasPermission } = useHelpers()
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
const canWrite = hasPermission('habit', 'write')
|
const canWrite = hasPermission(currentUser, 'habit', 'write')
|
||||||
const canInteract = hasPermission('habit', 'interact')
|
const canInteract = hasPermission(currentUser, 'habit', 'interact')
|
||||||
const [browserSettings] = useAtom(browserSettingsAtom)
|
const [browserSettings] = useAtom(browserSettingsAtom)
|
||||||
const isTasksView = browserSettings.viewType === 'tasks'
|
const isTasksView = browserSettings.viewType === 'tasks'
|
||||||
const isRecurRule = !isTasksView
|
const isRecurRule = !isTasksView
|
||||||
|
|||||||
@@ -6,21 +6,21 @@ import Navigation from './Navigation'
|
|||||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-screen bg-gray-100 dark:bg-gray-900 overflow-hidden">
|
<div className="flex flex-col h-screen bg-gray-100 dark:bg-gray-900 overflow-hidden">
|
||||||
<Header className="sticky top-0 z-50" />
|
<ClientWrapper>
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<Header className="sticky top-0 z-50" />
|
||||||
<Navigation viewPort='main' />
|
<div className="flex flex-1 overflow-hidden">
|
||||||
<div className="flex-1 flex flex-col">
|
<Navigation viewPort='main' />
|
||||||
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100 dark:bg-gray-900 relative">
|
<div className="flex-1 flex flex-col">
|
||||||
{/* responsive container (optimized for mobile) */}
|
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100 dark:bg-gray-900 relative">
|
||||||
<div className="mx-auto px-2 xs:px-4 py-8 max-w-sm xs:max-w-full">
|
{/* responsive container (optimized for mobile) */}
|
||||||
<ClientWrapper>
|
<div className="mx-auto px-2 xs:px-4 py-8 max-w-sm xs:max-w-full">
|
||||||
{children}
|
{children}
|
||||||
</ClientWrapper>
|
</div>
|
||||||
</div>
|
</main>
|
||||||
</main>
|
<Navigation viewPort='mobile' />
|
||||||
<Navigation viewPort='mobile' />
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ClientWrapper>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,24 @@ export interface NavItemType {
|
|||||||
|
|
||||||
interface MobileNavDisplayProps {
|
interface MobileNavDisplayProps {
|
||||||
navItems: NavItemType[];
|
navItems: NavItemType[];
|
||||||
isIOS: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MobileNavDisplay({ navItems, isIOS }: MobileNavDisplayProps) {
|
// detect iOS: https://stackoverflow.com/a/9039885
|
||||||
|
function iOS() {
|
||||||
|
return [
|
||||||
|
'iPad Simulator',
|
||||||
|
'iPhone Simulator',
|
||||||
|
'iPod Simulator',
|
||||||
|
'iPad',
|
||||||
|
'iPhone',
|
||||||
|
'iPod',
|
||||||
|
].includes(navigator.platform)
|
||||||
|
// iPad on iOS 13 detection
|
||||||
|
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export default function MobileNavDisplay({ navItems }: MobileNavDisplayProps) {
|
||||||
// Filter for items relevant to mobile view, typically 'main' and 'bottom' positions
|
// Filter for items relevant to mobile view, typically 'main' and 'bottom' positions
|
||||||
const mobileNavItems = navItems.filter(item => item.position === 'main' || item.position === 'bottom');
|
const mobileNavItems = navItems.filter(item => item.position === 'main' || item.position === 'bottom');
|
||||||
// The original code spread main and bottom items separately, effectively concatenating them.
|
// The original code spread main and bottom items separately, effectively concatenating them.
|
||||||
@@ -22,6 +36,7 @@ export default function MobileNavDisplay({ navItems, isIOS }: MobileNavDisplayPr
|
|||||||
// The original code: [...navItems(isTasksView).filter(item => item.position === 'main'), ...navItems(isTasksView).filter(item => item.position === 'bottom')]
|
// The original code: [...navItems(isTasksView).filter(item => item.position === 'main'), ...navItems(isTasksView).filter(item => item.position === 'bottom')]
|
||||||
// This implies that items could be in 'main' or 'bottom'. The current navItems only have 'main'.
|
// This implies that items could be in 'main' or 'bottom'. The current navItems only have 'main'.
|
||||||
// A simple combined list is fine.
|
// A simple combined list is fine.
|
||||||
|
const isIOS = iOS()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { browserSettingsAtom } from '@/lib/atoms'
|
|||||||
import { useEffect, useState, ElementType } from 'react'
|
import { useEffect, useState, ElementType } from 'react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
import { HabitIcon, TaskIcon } from '@/lib/constants'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import MobileNavDisplay from './MobileNavDisplay'
|
import MobileNavDisplay from './MobileNavDisplay'
|
||||||
import DesktopNavDisplay from './DesktopNavDisplay'
|
import DesktopNavDisplay from './DesktopNavDisplay'
|
||||||
|
|
||||||
@@ -24,12 +23,12 @@ interface NavigationProps {
|
|||||||
viewPort: ViewPort
|
viewPort: ViewPort
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default function Navigation({ className, viewPort }: NavigationProps) {
|
export default function Navigation({ className, viewPort }: NavigationProps) {
|
||||||
const t = useTranslations('Navigation')
|
const t = useTranslations('Navigation')
|
||||||
const [isMobileView, setIsMobileView] = useState(false)
|
const [isMobileView, setIsMobileView] = useState(false)
|
||||||
const [browserSettings] = useAtom(browserSettingsAtom)
|
const [browserSettings] = useAtom(browserSettingsAtom)
|
||||||
const isTasksView = browserSettings.viewType === 'tasks'
|
const isTasksView = browserSettings.viewType === 'tasks'
|
||||||
const { isIOS } = useHelpers()
|
|
||||||
|
|
||||||
const currentNavItems: NavItemType[] = [
|
const currentNavItems: NavItemType[] = [
|
||||||
{ icon: Home, label: t('dashboard'), href: '/', position: 'main' },
|
{ icon: Home, label: t('dashboard'), href: '/', position: 'main' },
|
||||||
@@ -60,7 +59,7 @@ export default function Navigation({ className, viewPort }: NavigationProps) {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (viewPort === 'mobile' && isMobileView) {
|
if (viewPort === 'mobile' && isMobileView) {
|
||||||
return <MobileNavDisplay navItems={currentNavItems} isIOS={isIOS} />
|
return <MobileNavDisplay navItems={currentNavItems} />
|
||||||
}
|
}
|
||||||
|
|
||||||
if (viewPort === 'main' && !isMobileView) {
|
if (viewPort === 'main' && !isMobileView) {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { coinsAtom, habitsAtom, wishlistAtom, usersAtom } from '@/lib/atoms'
|
import { coinsAtom, habitsAtom, wishlistAtom, usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||||
import { Bell } from 'lucide-react';
|
import { Bell } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
@@ -14,12 +14,11 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { updateLastNotificationReadTimestamp } from '@/app/actions/data';
|
import { updateLastNotificationReadTimestamp } from '@/app/actions/data';
|
||||||
import { d2t, getNow, t2d } from '@/lib/utils';
|
import { d2t, getNow, t2d } from '@/lib/utils';
|
||||||
import { useHelpers } from '@/lib/client-helpers';
|
|
||||||
import { User, CoinTransaction } from '@/lib/types';
|
import { User, CoinTransaction } from '@/lib/types';
|
||||||
|
|
||||||
export default function NotificationBell() {
|
export default function NotificationBell() {
|
||||||
const t = useTranslations('NotificationBell');
|
const t = useTranslations('NotificationBell');
|
||||||
const { currentUser } = useHelpers();
|
const [currentUser] = useAtom(currentUserAtom);
|
||||||
const [coinsData] = useAtom(coinsAtom)
|
const [coinsData] = useAtom(coinsAtom)
|
||||||
const [habitsData] = useAtom(habitsAtom)
|
const [habitsData] = useAtom(habitsAtom)
|
||||||
const [wishlistData] = useAtom(wishlistAtom)
|
const [wishlistData] = useAtom(wishlistAtom)
|
||||||
@@ -122,7 +121,7 @@ export default function NotificationBell() {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" className="p-0 w-80 md:w-96">
|
<DropdownMenuContent align="end" className="p-0 w-80 md:w-96">
|
||||||
<NotificationDropdown
|
<NotificationDropdown
|
||||||
currentUser={currentUser as User | null} // Cast needed as useHelpers can return undefined initially
|
currentUser={currentUser as User | null} // Cast needed as as currentUser can be undefined
|
||||||
unreadNotifications={unreadNotifications}
|
unreadNotifications={unreadNotifications}
|
||||||
displayedReadNotifications={displayedReadNotifications}
|
displayedReadNotifications={displayedReadNotifications}
|
||||||
habitsData={habitsData} // Pass necessary data down
|
habitsData={habitsData} // Pass necessary data down
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import { Dialog, DialogContent, DialogHeader, DialogTitle } from './ui/dialog'
|
|||||||
import UserForm from './UserForm'
|
import UserForm from './UserForm'
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { useAtom } from "jotai"
|
import { useAtom } from "jotai"
|
||||||
import { aboutOpenAtom, settingsAtom, userSelectAtom } from "@/lib/atoms"
|
import { aboutOpenAtom, settingsAtom, userSelectAtom, currentUserAtom } from "@/lib/atoms"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes"
|
||||||
import { signOut } from "@/app/actions/user"
|
import { signOut } from "@/app/actions/user"
|
||||||
import { toast } from "@/hooks/use-toast"
|
import { toast } from "@/hooks/use-toast"
|
||||||
import { useHelpers } from "@/lib/client-helpers"
|
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
|
|
||||||
export function Profile() {
|
export function Profile() {
|
||||||
@@ -23,7 +22,7 @@ export function Profile() {
|
|||||||
const [isEditing, setIsEditing] = useState(false)
|
const [isEditing, setIsEditing] = useState(false)
|
||||||
const [aboutOpen, setAboutOpen] = useAtom(aboutOpenAtom)
|
const [aboutOpen, setAboutOpen] = useAtom(aboutOpenAtom)
|
||||||
const { theme, setTheme } = useTheme()
|
const { theme, setTheme } = useTheme()
|
||||||
const { currentUser: user } = useHelpers()
|
const [user] = useAtom(currentUserAtom)
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ import { Switch } from './ui/switch';
|
|||||||
import { Permission } from '@/lib/types';
|
import { Permission } from '@/lib/types';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
import { serverSettingsAtom, usersAtom } from '@/lib/atoms';
|
import { serverSettingsAtom, usersAtom, currentUserAtom } from '@/lib/atoms';
|
||||||
import { createUser, updateUser, updateUserPassword, uploadAvatar } from '@/app/actions/data';
|
import { createUser, updateUser, updateUserPassword, uploadAvatar } from '@/app/actions/data';
|
||||||
import { SafeUser, User } from '@/lib/types';
|
import { SafeUser, User } from '@/lib/types';
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar';
|
||||||
import { User as UserIcon } from 'lucide-react';
|
import { User as UserIcon } from 'lucide-react';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { PermissionSelector } from './PermissionSelector';
|
import { PermissionSelector } from './PermissionSelector';
|
||||||
import { useHelpers } from '@/lib/client-helpers';
|
|
||||||
|
|
||||||
interface UserFormProps {
|
interface UserFormProps {
|
||||||
userId?: string; // if provided, we're editing; if not, we're creating
|
userId?: string; // if provided, we're editing; if not, we're creating
|
||||||
@@ -41,7 +41,7 @@ export default function UserForm({ userId, onCancel, onSuccess }: UserFormProps)
|
|||||||
const [users, setUsersData] = useAtom(usersAtom);
|
const [users, setUsersData] = useAtom(usersAtom);
|
||||||
const serverSettings = useAtomValue(serverSettingsAtom)
|
const serverSettings = useAtomValue(serverSettingsAtom)
|
||||||
const user = userId ? users.users.find(u => u.id === userId) : undefined;
|
const user = userId ? users.users.find(u => u.id === userId) : undefined;
|
||||||
const { currentUser } = useHelpers()
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
const getDefaultPermissions = (): Permission[] => [{
|
const getDefaultPermissions = (): Permission[] => [{
|
||||||
habit: {
|
habit: {
|
||||||
write: true,
|
write: true,
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog"
|
} from "@/components/ui/alert-dialog"
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { usersAtom } from '@/lib/atoms';
|
import { usersAtom, currentUserAtom } from '@/lib/atoms';
|
||||||
import { signIn } from '@/app/actions/user';
|
import { signIn } from '@/app/actions/user';
|
||||||
import { createUser } from '@/app/actions/data';
|
import { createUser } from '@/app/actions/data';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
@@ -28,7 +28,7 @@ import { toast } from '@/hooks/use-toast';
|
|||||||
import { Description } from '@radix-ui/react-dialog';
|
import { Description } from '@radix-ui/react-dialog';
|
||||||
import { SafeUser, User } from '@/lib/types';
|
import { SafeUser, User } from '@/lib/types';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { useHelpers } from '@/lib/client-helpers';
|
|
||||||
|
|
||||||
function UserCard({
|
function UserCard({
|
||||||
user,
|
user,
|
||||||
@@ -145,7 +145,7 @@ export default function UserSelectModal({ onClose }: { onClose: () => void }) {
|
|||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [usersData, setUsersData] = useAtom(usersAtom);
|
const [usersData, setUsersData] = useAtom(usersAtom);
|
||||||
const users = usersData.users;
|
const users = usersData.users;
|
||||||
const { currentUser } = useHelpers();
|
const [currentUser] = useAtom(currentUserAtom);
|
||||||
|
|
||||||
|
|
||||||
const handleUserSelect = (userId: string) => {
|
const handleUserSelect = (userId: string) => {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { WishlistItemType, User } from '@/lib/types'
|
import { WishlistItemType, User } from '@/lib/types'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { usersAtom } from '@/lib/atoms'
|
import { usersAtom, currentUserAtom } from '@/lib/atoms'
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
import { hasPermission } from '@/lib/utils'
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -60,9 +60,9 @@ export default function WishlistItem({
|
|||||||
isRecentlyRedeemed
|
isRecentlyRedeemed
|
||||||
}: WishlistItemProps) {
|
}: WishlistItemProps) {
|
||||||
const t = useTranslations('WishlistItem')
|
const t = useTranslations('WishlistItem')
|
||||||
const { currentUser, hasPermission } = useHelpers()
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
const canWrite = hasPermission('wishlist', 'write')
|
const canWrite = hasPermission(currentUser, 'wishlist', 'write')
|
||||||
const canInteract = hasPermission('wishlist', 'interact')
|
const canInteract = hasPermission(currentUser, 'wishlist', 'interact')
|
||||||
const [usersData] = useAtom(usersAtom)
|
const [usersData] = useAtom(usersAtom)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai';
|
||||||
import { useTranslations } from 'next-intl'
|
import { useState, useEffect } from 'react';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import { calculateCoinsEarnedToday, calculateCoinsSpentToday, calculateTotalEarned, calculateTotalSpent, calculateTransactionsToday, checkPermission } from '@/lib/utils'
|
import { calculateCoinsEarnedToday, calculateCoinsSpentToday, calculateTotalEarned, calculateTotalSpent, calculateTransactionsToday, checkPermission } from '@/lib/utils'
|
||||||
import {
|
import {
|
||||||
coinsAtom,
|
coinsAtom,
|
||||||
@@ -11,11 +12,11 @@ import {
|
|||||||
coinsBalanceAtom,
|
coinsBalanceAtom,
|
||||||
settingsAtom,
|
settingsAtom,
|
||||||
usersAtom,
|
usersAtom,
|
||||||
|
currentUserAtom,
|
||||||
} from '@/lib/atoms'
|
} from '@/lib/atoms'
|
||||||
import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data'
|
import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data'
|
||||||
import { CoinsData, User } from '@/lib/types'
|
import { CoinsData, User } from '@/lib/types'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
||||||
|
|
||||||
function handlePermissionCheck(
|
function handlePermissionCheck(
|
||||||
@@ -51,23 +52,58 @@ export function useCoins(options?: { selectedUser?: string }) {
|
|||||||
const [coins, setCoins] = useAtom(coinsAtom)
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const [users] = useAtom(usersAtom)
|
const [users] = useAtom(usersAtom)
|
||||||
const { currentUser } = useHelpers()
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
let user: User | undefined;
|
const [allCoinsData] = useAtom(coinsAtom) // All coin transactions
|
||||||
if (!options?.selectedUser) {
|
const [loggedInUserBalance] = useAtom(coinsBalanceAtom) // Balance of the *currently logged-in* user
|
||||||
user = currentUser;
|
const [atomCoinsEarnedToday] = useAtom(coinsEarnedTodayAtom);
|
||||||
} else {
|
const [atomTotalEarned] = useAtom(totalEarnedAtom)
|
||||||
user = users.users.find(u => u.id === options.selectedUser)
|
const [atomTotalSpent] = useAtom(totalSpentAtom)
|
||||||
}
|
const [atomCoinsSpentToday] = useAtom(coinsSpentTodayAtom);
|
||||||
|
const [atomTransactionsToday] = useAtom(transactionsTodayAtom);
|
||||||
|
const targetUser = options?.selectedUser ? users.users.find(u => u.id === options.selectedUser) : currentUser
|
||||||
|
// Filter transactions for the targetUser
|
||||||
|
const transactions = allCoinsData.transactions.filter(t => t.userId === targetUser?.id)
|
||||||
|
const timezone = settings.system.timezone;
|
||||||
|
const [coinsEarnedToday, setCoinsEarnedToday] = useState(0);
|
||||||
|
const [totalEarned, setTotalEarned] = useState(0);
|
||||||
|
const [totalSpent, setTotalSpent] = useState(0);
|
||||||
|
const [coinsSpentToday, setCoinsSpentToday] = useState(0);
|
||||||
|
const [transactionsToday, setTransactionsToday] = useState<number>(0);
|
||||||
|
const [balance, setBalance] = useState(0);
|
||||||
|
|
||||||
// Filter transactions for the selectd user
|
useEffect(() => {
|
||||||
const transactions = coins.transactions.filter(t => t.userId === user?.id)
|
// Calculate other metrics
|
||||||
|
if (targetUser?.id && targetUser.id === currentUser?.id) {
|
||||||
const [balance] = useAtom(coinsBalanceAtom)
|
// If the target user is the currently logged-in user, use the derived atom's value
|
||||||
const [coinsEarnedToday] = useAtom(coinsEarnedTodayAtom)
|
setCoinsEarnedToday(atomCoinsEarnedToday);
|
||||||
const [totalEarned] = useAtom(totalEarnedAtom)
|
setTotalEarned(atomTotalEarned);
|
||||||
const [totalSpent] = useAtom(totalSpentAtom)
|
setTotalSpent(atomTotalSpent);
|
||||||
const [coinsSpentToday] = useAtom(coinsSpentTodayAtom)
|
setCoinsSpentToday(atomCoinsSpentToday);
|
||||||
const [transactionsToday] = useAtom(transactionsTodayAtom)
|
setTransactionsToday(atomTransactionsToday);
|
||||||
|
setBalance(loggedInUserBalance);
|
||||||
|
} else if (targetUser?.id) {
|
||||||
|
// If an admin is viewing another user, calculate their metrics manually
|
||||||
|
setCoinsEarnedToday(calculateCoinsEarnedToday(transactions, timezone));
|
||||||
|
setTotalEarned(calculateTotalEarned(transactions));
|
||||||
|
setTotalSpent(calculateTotalSpent(transactions));
|
||||||
|
setCoinsSpentToday(calculateCoinsSpentToday(transactions, timezone));
|
||||||
|
setTransactionsToday(calculateTransactionsToday(transactions, timezone));
|
||||||
|
const userTransactions = allCoinsData.transactions.filter(t => t.userId === targetUser!.id);
|
||||||
|
setBalance(userTransactions.reduce((acc, t) => acc + t.amount, 0));
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
targetUser,
|
||||||
|
currentUser,
|
||||||
|
transactions, // Derived from allCoinsData and targetUser
|
||||||
|
allCoinsData, // Added for balance calculation when targetUser is not currentUser
|
||||||
|
timezone,
|
||||||
|
loggedInUserBalance, // Added for balance calculation when targetUser is currentUser
|
||||||
|
atomCoinsEarnedToday,
|
||||||
|
atomTotalEarned,
|
||||||
|
atomTotalSpent,
|
||||||
|
atomCoinsSpentToday,
|
||||||
|
atomTransactionsToday,
|
||||||
|
]);
|
||||||
|
|
||||||
const add = async (amount: number, description: string, note?: string) => {
|
const add = async (amount: number, description: string, note?: string) => {
|
||||||
if (!handlePermissionCheck(currentUser, 'coins', 'write', tCommon)) return null
|
if (!handlePermissionCheck(currentUser, 'coins', 'write', tCommon)) return null
|
||||||
@@ -91,7 +127,7 @@ export function useCoins(options?: { selectedUser?: string }) {
|
|||||||
description,
|
description,
|
||||||
type: 'MANUAL_ADJUSTMENT',
|
type: 'MANUAL_ADJUSTMENT',
|
||||||
note,
|
note,
|
||||||
userId: user?.id
|
userId: targetUser?.id
|
||||||
})
|
})
|
||||||
setCoins(data)
|
setCoins(data)
|
||||||
toast({ title: t("successTitle"), description: t("addedCoinsDescription", { amount }) })
|
toast({ title: t("successTitle"), description: t("addedCoinsDescription", { amount }) })
|
||||||
@@ -121,7 +157,7 @@ export function useCoins(options?: { selectedUser?: string }) {
|
|||||||
description,
|
description,
|
||||||
type: 'MANUAL_ADJUSTMENT',
|
type: 'MANUAL_ADJUSTMENT',
|
||||||
note,
|
note,
|
||||||
userId: user?.id
|
userId: targetUser?.id
|
||||||
})
|
})
|
||||||
setCoins(data)
|
setCoins(data)
|
||||||
toast({ title: t("successTitle"), description: t("removedCoinsDescription", { amount: numAmount }) })
|
toast({ title: t("successTitle"), description: t("removedCoinsDescription", { amount: numAmount }) })
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAtom, atom } from 'jotai'
|
import { useAtom, atom } from 'jotai'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { habitsAtom, coinsAtom, settingsAtom, usersAtom, habitFreqMapAtom } from '@/lib/atoms'
|
import { habitsAtom, coinsAtom, settingsAtom, usersAtom, habitFreqMapAtom, currentUserAtom } from '@/lib/atoms'
|
||||||
import { addCoins, removeCoins, saveHabitsData } from '@/app/actions/data'
|
import { addCoins, removeCoins, saveHabitsData } from '@/app/actions/data'
|
||||||
import { Habit, Permission, SafeUser, User } from '@/lib/types'
|
import { Habit, Permission, SafeUser, User } from '@/lib/types'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
@@ -20,10 +20,10 @@ import {
|
|||||||
} from '@/lib/utils'
|
} from '@/lib/utils'
|
||||||
import { ToastAction } from '@/components/ui/toast'
|
import { ToastAction } from '@/components/ui/toast'
|
||||||
import { Undo2 } from 'lucide-react'
|
import { Undo2 } from 'lucide-react'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
|
|
||||||
function handlePermissionCheck(
|
function handlePermissionCheck(
|
||||||
user: SafeUser | undefined,
|
user: SafeUser | User | undefined,
|
||||||
resource: 'habit' | 'wishlist' | 'coins',
|
resource: 'habit' | 'wishlist' | 'coins',
|
||||||
action: 'write' | 'interact',
|
action: 'write' | 'interact',
|
||||||
tCommon: (key: string, values?: Record<string, any>) => string
|
tCommon: (key: string, values?: Record<string, any>) => string
|
||||||
@@ -54,7 +54,7 @@ export function useHabits() {
|
|||||||
const t = useTranslations('useHabits');
|
const t = useTranslations('useHabits');
|
||||||
const tCommon = useTranslations('Common');
|
const tCommon = useTranslations('Common');
|
||||||
const [usersData] = useAtom(usersAtom)
|
const [usersData] = useAtom(usersAtom)
|
||||||
const { currentUser } = useHelpers()
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
const [habitsData, setHabitsData] = useAtom(habitsAtom)
|
const [habitsData, setHabitsData] = useAtom(habitsAtom)
|
||||||
const [coins, setCoins] = useAtom(coinsAtom)
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
|
|||||||
@@ -1,16 +1,15 @@
|
|||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { wishlistAtom, coinsAtom } from '@/lib/atoms'
|
import { wishlistAtom, coinsAtom, currentUserAtom } from '@/lib/atoms'
|
||||||
import { saveWishlistItems, removeCoins } from '@/app/actions/data'
|
import { saveWishlistItems, removeCoins } from '@/app/actions/data'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { WishlistItemType } from '@/lib/types'
|
import { WishlistItemType, User, SafeUser } from '@/lib/types'
|
||||||
import { celebrations } from '@/utils/celebrations'
|
import { celebrations } from '@/utils/celebrations'
|
||||||
import { checkPermission } from '@/lib/utils'
|
import { checkPermission } from '@/lib/utils'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import { useCoins } from './useCoins'
|
import { useCoins } from './useCoins'
|
||||||
|
|
||||||
function handlePermissionCheck(
|
function handlePermissionCheck(
|
||||||
user: any, // Consider using a more specific type like SafeUser | User | undefined
|
user: User | SafeUser | undefined,
|
||||||
resource: 'habit' | 'wishlist' | 'coins',
|
resource: 'habit' | 'wishlist' | 'coins',
|
||||||
action: 'write' | 'interact',
|
action: 'write' | 'interact',
|
||||||
tCommon: (key: string, values?: Record<string, any>) => string
|
tCommon: (key: string, values?: Record<string, any>) => string
|
||||||
@@ -39,7 +38,7 @@ function handlePermissionCheck(
|
|||||||
export function useWishlist() {
|
export function useWishlist() {
|
||||||
const t = useTranslations('useWishlist');
|
const t = useTranslations('useWishlist');
|
||||||
const tCommon = useTranslations('Common');
|
const tCommon = useTranslations('Common');
|
||||||
const { currentUser: user } = useHelpers()
|
const [user] = useAtom(currentUserAtom)
|
||||||
const [wishlist, setWishlist] = useAtom(wishlistAtom)
|
const [wishlist, setWishlist] = useAtom(wishlistAtom)
|
||||||
const [coins, setCoins] = useAtom(coinsAtom)
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
const { balance } = useCoins()
|
const { balance } = useCoins()
|
||||||
|
|||||||
21
lib/atoms.ts
21
lib/atoms.ts
@@ -10,6 +10,7 @@ import {
|
|||||||
CompletionCache,
|
CompletionCache,
|
||||||
getDefaultServerSettings,
|
getDefaultServerSettings,
|
||||||
User,
|
User,
|
||||||
|
UserId,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import {
|
import {
|
||||||
getTodayInTimezone,
|
getTodayInTimezone,
|
||||||
@@ -85,10 +86,26 @@ export const transactionsTodayAtom = atom((get) => {
|
|||||||
return calculateTransactionsToday(coins.transactions, settings.system.timezone);
|
return calculateTransactionsToday(coins.transactions, settings.system.timezone);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derived atom for current balance from all transactions
|
// Atom to store the current logged-in user's ID.
|
||||||
|
// This should be set by your application when the user session is available.
|
||||||
|
export const currentUserIdAtom = atom<UserId | undefined>(undefined);
|
||||||
|
|
||||||
|
export const currentUserAtom = atom((get) => {
|
||||||
|
const currentUserId = get(currentUserIdAtom);
|
||||||
|
const users = get(usersAtom);
|
||||||
|
return users.users.find(user => user.id === currentUserId);
|
||||||
|
})
|
||||||
|
|
||||||
|
// Derived atom for current balance for the logged-in user
|
||||||
export const coinsBalanceAtom = atom((get) => {
|
export const coinsBalanceAtom = atom((get) => {
|
||||||
|
const loggedInUserId = get(currentUserIdAtom);
|
||||||
|
if (!loggedInUserId) {
|
||||||
|
return 0; // No user logged in or ID not set, so balance is 0
|
||||||
|
}
|
||||||
const coins = get(coinsAtom);
|
const coins = get(coinsAtom);
|
||||||
return coins.transactions.reduce((sum, transaction) => sum + transaction.amount, 0);
|
return coins.transactions
|
||||||
|
.filter(transaction => transaction.userId === loggedInUserId)
|
||||||
|
.reduce((sum, transaction) => sum + transaction.amount, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
/* transient atoms */
|
/* transient atoms */
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
// client helpers
|
|
||||||
'use-client'
|
|
||||||
|
|
||||||
import { useSession } from "next-auth/react"
|
|
||||||
import { User, UserId } from './types'
|
|
||||||
import { useAtom } from 'jotai'
|
|
||||||
import { usersAtom } from './atoms'
|
|
||||||
import { checkPermission } from './utils'
|
|
||||||
|
|
||||||
export function useHelpers() {
|
|
||||||
const { data: session, status } = useSession()
|
|
||||||
const currentUserId = session?.user.id
|
|
||||||
const [usersData] = useAtom(usersAtom)
|
|
||||||
const currentUser = usersData.users.find((u) => u.id === currentUserId)
|
|
||||||
// detect iOS: https://stackoverflow.com/a/9039885
|
|
||||||
function iOS() {
|
|
||||||
return [
|
|
||||||
'iPad Simulator',
|
|
||||||
'iPhone Simulator',
|
|
||||||
'iPod Simulator',
|
|
||||||
'iPad',
|
|
||||||
'iPhone',
|
|
||||||
'iPod',
|
|
||||||
].includes(navigator.platform)
|
|
||||||
// iPad on iOS 13 detection
|
|
||||||
|| (navigator.userAgent.includes("Mac") && "ontouchend" in document)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
currentUserId,
|
|
||||||
currentUser,
|
|
||||||
usersData,
|
|
||||||
status,
|
|
||||||
hasPermission: (resource: 'habit' | 'wishlist' | 'coins', action: 'write' | 'interact') => currentUser?.isAdmin ||
|
|
||||||
checkPermission(currentUser?.permissions, resource, action),
|
|
||||||
isIOS: iOS(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
19
lib/utils.ts
19
lib/utils.ts
@@ -2,7 +2,7 @@ import { clsx, type ClassValue } from "clsx"
|
|||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
import { DateTime, DateTimeFormatOptions } from "luxon"
|
import { DateTime, DateTimeFormatOptions } from "luxon"
|
||||||
import { datetime, RRule } from 'rrule'
|
import { datetime, RRule } from 'rrule'
|
||||||
import { Freq, Habit, CoinTransaction, Permission, ParsedFrequencyResult, ParsedResultType } from '@/lib/types'
|
import { Freq, Habit, CoinTransaction, Permission, ParsedFrequencyResult, ParsedResultType, User } from '@/lib/types'
|
||||||
import { DUE_MAP, INITIAL_DUE, RECURRENCE_RULE_MAP } from "./constants"
|
import { DUE_MAP, INITIAL_DUE, RECURRENCE_RULE_MAP } from "./constants"
|
||||||
import * as chrono from 'chrono-node'
|
import * as chrono from 'chrono-node'
|
||||||
import _ from "lodash"
|
import _ from "lodash"
|
||||||
@@ -464,3 +464,20 @@ export function checkPermission(
|
|||||||
export function uuid() {
|
export function uuid() {
|
||||||
return uuidv4()
|
return uuidv4()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function hasPermission(
|
||||||
|
currentUser: User | undefined,
|
||||||
|
resource: 'habit' | 'wishlist' | 'coins',
|
||||||
|
action: 'write' | 'interact'
|
||||||
|
): boolean {
|
||||||
|
// If no current user, no permissions.
|
||||||
|
if (!currentUser) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// If user is admin, they have all permissions.
|
||||||
|
if (currentUser.isAdmin) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Otherwise, check specific permissions.
|
||||||
|
return checkPermission(currentUser.permissions, resource, action);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "habittrove",
|
"name": "habittrove",
|
||||||
"version": "0.2.19",
|
"version": "0.2.20",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
|
|||||||
Reference in New Issue
Block a user