mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
Merge Tag v0.2.20
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
|
||||||
|
|||||||
@@ -7,8 +7,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { settingsAtom, usersAtom } from '@/lib/atoms'
|
import { currentUserAtom, settingsAtom, usersAtom } from '@/lib/atoms'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, MAX_COIN_LIMIT, QUICK_DATES } from '@/lib/constants'
|
import { INITIAL_DUE, INITIAL_RECURRENCE_RULE, MAX_COIN_LIMIT, QUICK_DATES } from '@/lib/constants'
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2t, serializeRRule } from '@/lib/utils'
|
import { convertHumanReadableFrequencyToMachineReadable, convertMachineReadableFrequencyToHumanReadable, d2t, serializeRRule } from '@/lib/utils'
|
||||||
@@ -20,6 +19,7 @@ import { useState } from 'react'
|
|||||||
import { RRule } from 'rrule'
|
import { RRule } from 'rrule'
|
||||||
import EmojiPickerButton from './EmojiPickerButton'
|
import EmojiPickerButton from './EmojiPickerButton'
|
||||||
|
|
||||||
|
|
||||||
interface AddEditHabitModalProps {
|
interface AddEditHabitModalProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
onSave: (habit: Omit<Habit, 'id'>) => Promise<void>
|
onSave: (habit: Omit<Habit, 'id'>) => Promise<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 [selectedUserIds, setSelectedUserIds] = useState<string[]>((habit?.userIds || []).filter(id => id !== currentUser?.id))
|
const [selectedUserIds, setSelectedUserIds] = useState<string[]>((habit?.userIds || []).filter(id => id !== currentUser?.id))
|
||||||
const [usersData] = useAtom(usersAtom)
|
const [usersData] = useAtom(usersAtom)
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { usersAtom } from '@/lib/atoms'
|
import { currentUserAtom, usersAtom } from '@/lib/atoms'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
||||||
import { WishlistItemType } from '@/lib/types'
|
import { WishlistItemType } from '@/lib/types'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
@@ -37,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,18 +1,19 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { ReactNode, Suspense, useEffect, useState } from 'react'
|
import { aboutOpenAtom, currentUserIdAtom, pomodoroAtom, userSelectAtom } from '@/lib/atoms';
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom, useSetAtom } from 'jotai';
|
||||||
import { aboutOpenAtom, pomodoroAtom, userSelectAtom } from '@/lib/atoms'
|
|
||||||
import PomodoroTimer from './PomodoroTimer'
|
|
||||||
import UserSelectModal from './UserSelectModal'
|
|
||||||
import { useSession } from 'next-auth/react'
|
import { useSession } from 'next-auth/react'
|
||||||
|
import { ReactNode, useEffect, useState } from 'react'
|
||||||
import AboutModal from './AboutModal'
|
import AboutModal from './AboutModal'
|
||||||
import LoadingSpinner from './LoadingSpinner'
|
import LoadingSpinner from './LoadingSpinner'
|
||||||
|
import PomodoroTimer from './PomodoroTimer'
|
||||||
|
import UserSelectModal from './UserSelectModal'
|
||||||
|
|
||||||
export default function ClientWrapper({ children }: { children: ReactNode }) {
|
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);
|
||||||
@@ -29,6 +30,10 @@ export default function ClientWrapper({ children }: { children: ReactNode }) {
|
|||||||
}
|
}
|
||||||
}, [currentUserId, status, userSelect, setUserSelect])
|
}, [currentUserId, status, userSelect, setUserSelect])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setCurrentUserIdAtom(currentUserId)
|
||||||
|
}, [currentUserId, setCurrentUserIdAtom])
|
||||||
|
|
||||||
if (!isMounted) {
|
if (!isMounted) {
|
||||||
return <LoadingSpinner />
|
return <LoadingSpinner />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { Button } from '@/components/ui/button'
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { useCoins } from '@/hooks/useCoins'
|
import { useCoins } from '@/hooks/useCoins'
|
||||||
import { settingsAtom, usersAtom } from '@/lib/atoms'
|
import { currentUserAtom, settingsAtom, usersAtom } from '@/lib/atoms'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
||||||
import { TransactionType } from '@/lib/types'
|
import { TransactionType } from '@/lib/types'
|
||||||
import { d2s, t2d } from '@/lib/utils'
|
import { d2s, t2d } from '@/lib/utils'
|
||||||
@@ -22,7 +21,7 @@ import { TransactionNoteEditor } from './TransactionNoteEditor'
|
|||||||
|
|
||||||
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;
|
||||||
|
|||||||
@@ -6,12 +6,11 @@ import {
|
|||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { useHabits } from '@/hooks/useHabits'
|
import { useHabits } from '@/hooks/useHabits'
|
||||||
import { settingsAtom, usersAtom } from '@/lib/atoms'
|
import { currentUserAtom, settingsAtom, usersAtom } from '@/lib/atoms'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import { Habit, User } from '@/lib/types'
|
import { Habit, User } from '@/lib/types'
|
||||||
import { convertMachineReadableFrequencyToHumanReadable, getCompletionsForToday, isTaskOverdue } from '@/lib/utils'
|
import { convertMachineReadableFrequencyToHumanReadable, getCompletionsForToday, hasPermission, isTaskOverdue } from '@/lib/utils'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { Check, Coins, Edit, MoreVertical, Pin, Undo2 } from 'lucide-react'; // Removed unused icons
|
import { Check, Coins, Edit, MoreVertical, Pin, Undo2 } from 'lucide-react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -45,7 +44,7 @@ const renderUserAvatars = (habit: Habit, currentUser: User | null, usersData: {
|
|||||||
|
|
||||||
|
|
||||||
export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||||
const { completeHabit, undoComplete, archiveHabit, unarchiveHabit, saveHabit } = useHabits()
|
const { completeHabit, undoComplete } = useHabits()
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const completionsToday = getCompletionsForToday({ habit, timezone: settings.system.timezone })
|
const completionsToday = getCompletionsForToday({ habit, timezone: settings.system.timezone })
|
||||||
const target = habit.targetCompletions || 1
|
const target = habit.targetCompletions || 1
|
||||||
@@ -53,10 +52,10 @@ 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 canWrite = hasPermission('habit', 'write')
|
|
||||||
const canInteract = hasPermission('habit', 'interact')
|
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
|
const canWrite = hasPermission(currentUser, 'habit', 'write')
|
||||||
|
const canInteract = hasPermission(currentUser, 'habit', 'interact')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(window.location.search)
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import { NavDisplayProps, NavItemType } from './Navigation';
|
|
||||||
import { usePathname } from 'next/navigation';
|
|
||||||
import { useHelpers } from '@/lib/client-helpers';
|
import { useHelpers } from '@/lib/client-helpers';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { NavDisplayProps } from './Navigation';
|
||||||
|
|
||||||
export default function MobileNavDisplay({ navItems }: NavDisplayProps) {
|
export default function MobileNavDisplay({ navItems }: NavDisplayProps) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
|||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
import { toast } from "@/hooks/use-toast"
|
import { toast } from "@/hooks/use-toast"
|
||||||
import { aboutOpenAtom, settingsAtom, userSelectAtom } from "@/lib/atoms"
|
import { aboutOpenAtom, currentUserAtom, settingsAtom, userSelectAtom } from "@/lib/atoms"
|
||||||
import { useHelpers } from "@/lib/client-helpers"
|
|
||||||
import { useAtom } from "jotai"
|
import { useAtom } from "jotai"
|
||||||
import { ArrowRightLeft, Crown, Info, LogOut, Moon, Palette, Settings, Sun, User } from "lucide-react"
|
import { ArrowRightLeft, Crown, Info, LogOut, Moon, Palette, Settings, Sun, User } from "lucide-react"
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
AlertDialogTrigger,
|
AlertDialogTrigger,
|
||||||
} from "@/components/ui/alert-dialog";
|
} from "@/components/ui/alert-dialog";
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import { serverSettingsAtom, usersAtom } from '@/lib/atoms';
|
import { currentUserAtom, serverSettingsAtom, usersAtom } from '@/lib/atoms';
|
||||||
import { useHelpers } from '@/lib/client-helpers';
|
|
||||||
import { Permission } from '@/lib/types';
|
import { Permission } from '@/lib/types';
|
||||||
import { passwordSchema, usernameSchema } from '@/lib/zod';
|
import { passwordSchema, usernameSchema } from '@/lib/zod';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { useAtom, useAtomValue } from 'jotai';
|
||||||
@@ -29,6 +28,7 @@ import { Input } from './ui/input';
|
|||||||
import { Label } from './ui/label';
|
import { Label } from './ui/label';
|
||||||
import { Switch } from './ui/switch';
|
import { Switch } from './ui/switch';
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
@@ -40,7 +40,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,
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import { signIn } from '@/app/actions/user';
|
import { signIn } from '@/app/actions/user';
|
||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import { usersAtom } from '@/lib/atoms';
|
import { currentUserAtom, usersAtom } from '@/lib/atoms';
|
||||||
import { useHelpers } from '@/lib/client-helpers';
|
|
||||||
import { SafeUser, User } from '@/lib/types';
|
import { SafeUser, User } from '@/lib/types';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Description } from '@radix-ui/react-dialog';
|
import { Description } from '@radix-ui/react-dialog';
|
||||||
@@ -131,7 +130,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) => {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import {
|
|||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from '@/components/ui/dropdown-menu'
|
} from '@/components/ui/dropdown-menu'
|
||||||
import { usersAtom } from '@/lib/atoms'
|
import { currentUserAtom, usersAtom } from '@/lib/atoms'
|
||||||
import { useHelpers } from '@/lib/client-helpers'
|
|
||||||
import { User, WishlistItemType } from '@/lib/types'
|
import { User, WishlistItemType } from '@/lib/types'
|
||||||
|
import { hasPermission } from '@/lib/utils'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { Archive, ArchiveRestore, Coins, Edit, Gift, MoreVertical, Trash2 } from 'lucide-react'
|
import { Archive, ArchiveRestore, Coins, Edit, Gift, MoreVertical, Trash2 } from 'lucide-react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
@@ -59,9 +59,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, useMemo } 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,59 @@ 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 selectd user
|
const transactions = useMemo(() => {
|
||||||
const transactions = coins.transactions.filter(t => t.userId === user?.id)
|
return allCoinsData.transactions.filter(t => t.userId === targetUser?.id);
|
||||||
|
}, [allCoinsData, targetUser?.id]);
|
||||||
|
|
||||||
const [balance] = useAtom(coinsBalanceAtom)
|
const timezone = settings.system.timezone;
|
||||||
const [coinsEarnedToday] = useAtom(coinsEarnedTodayAtom)
|
const [coinsEarnedToday, setCoinsEarnedToday] = useState(0);
|
||||||
const [totalEarned] = useAtom(totalEarnedAtom)
|
const [totalEarned, setTotalEarned] = useState(0);
|
||||||
const [totalSpent] = useAtom(totalSpentAtom)
|
const [totalSpent, setTotalSpent] = useState(0);
|
||||||
const [coinsSpentToday] = useAtom(coinsSpentTodayAtom)
|
const [coinsSpentToday, setCoinsSpentToday] = useState(0);
|
||||||
const [transactionsToday] = useAtom(transactionsTodayAtom)
|
const [transactionsToday, setTransactionsToday] = useState<number>(0);
|
||||||
|
const [balance, setBalance] = useState(0);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Calculate other metrics
|
||||||
|
if (targetUser?.id && targetUser.id === currentUser?.id) {
|
||||||
|
// If the target user is the currently logged-in user, use the derived atom's value
|
||||||
|
setCoinsEarnedToday(atomCoinsEarnedToday);
|
||||||
|
setTotalEarned(atomTotalEarned);
|
||||||
|
setTotalSpent(atomTotalSpent);
|
||||||
|
setCoinsSpentToday(atomCoinsSpentToday);
|
||||||
|
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));
|
||||||
|
setBalance(transactions.reduce((acc, t) => acc + t.amount, 0));
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
targetUser?.id,
|
||||||
|
currentUser?.id,
|
||||||
|
transactions, // Memoized: depends on allCoinsData and targetUser?.id
|
||||||
|
timezone,
|
||||||
|
loggedInUserBalance,
|
||||||
|
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 +128,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 +158,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()
|
||||||
|
|||||||
23
lib/atoms.ts
23
lib/atoms.ts
@@ -22,7 +22,8 @@ import {
|
|||||||
getDefaultSettings,
|
getDefaultSettings,
|
||||||
getDefaultUsersData,
|
getDefaultUsersData,
|
||||||
getDefaultWishlistData,
|
getDefaultWishlistData,
|
||||||
Habit
|
Habit,
|
||||||
|
UserId
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export interface BrowserSettings {
|
export interface BrowserSettings {
|
||||||
@@ -77,10 +78,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 */
|
||||||
|
|||||||
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