diff --git a/CHANGELOG.md b/CHANGELOG.md index a3a94cd..ce306c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Version 0.1.8 + +### Changed + +- use jotai for all state management + ## Version 0.1.7 ### Fixed diff --git a/app/actions/data.ts b/app/actions/data.ts index 3457fd8..22a24b6 100644 --- a/app/actions/data.ts +++ b/app/actions/data.ts @@ -14,7 +14,7 @@ import { DATA_DEFAULTS, getDefaultSettings } from '@/lib/types' -import { d2t, getNow, getNowInMilliseconds } from '@/lib/utils'; +import { d2t, getNow } from '@/lib/utils'; function getDefaultData(type: DataType): T { return DATA_DEFAULTS[type]() as T; @@ -65,8 +65,12 @@ async function saveData(type: DataType, data: T): Promise { } // Wishlist specific functions +export async function loadWishlistData(): Promise { + return loadData('wishlist') +} + export async function loadWishlistItems(): Promise { - const data = await loadData('wishlist') + const data = await loadWishlistData() return data.items } diff --git a/app/layout.tsx b/app/layout.tsx index 3c24b14..5dce74b 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -5,7 +5,7 @@ import { Toaster } from '@/components/ui/toaster' import { JotaiProvider } from '@/components/jotai-providers' import { Suspense } from 'react' import { JotaiHydrate } from '@/components/jotai-hydrate' -import { loadSettings } from './actions/data' +import { loadSettings, loadHabitsData, loadCoinsData, loadWishlistData } from './actions/data' // Inter (clean, modern, excellent readability) const inter = Inter({ subsets: ['latin'], @@ -32,13 +32,26 @@ export default async function RootLayout({ }: { children: React.ReactNode }) { - const initialSettings = await loadSettings() + const [initialSettings, initialHabits, initialCoins, initialWishlist] = await Promise.all([ + loadSettings(), + loadHabitsData(), + loadCoinsData(), + loadWishlistData() + ]) + return ( - + {children} diff --git a/components/CoinsManager.tsx b/components/CoinsManager.tsx index 813ae64..31e2e89 100644 --- a/components/CoinsManager.tsx +++ b/components/CoinsManager.tsx @@ -8,31 +8,25 @@ import { History } from 'lucide-react' import EmptyState from './EmptyState' import { Input } from '@/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { toast } from '@/hooks/use-toast' -import { useCoins } from '@/hooks/useCoins' +import { settingsAtom } from '@/lib/atoms' import Link from 'next/link' import { useAtom } from 'jotai' -import { settingsAtom } from '@/lib/atoms' +import { useCoins } from '@/hooks/useCoins' export default function CoinsManager() { - const { balance, transactions, addAmount, removeAmount } = useCoins() + const { add, remove, balance, transactions } = useCoins() const [settings] = useAtom(settingsAtom) const DEFAULT_AMOUNT = '0' const [amount, setAmount] = useState(DEFAULT_AMOUNT) - const handleAddCoins = async () => { - const data = await addAmount(Number(amount), "Manual addition") - if (data) { + const handleAddRemoveCoins = async () => { + const numAmount = Number(amount) + if (numAmount > 0) { + await add(numAmount, "Manual addition") setAmount(DEFAULT_AMOUNT) - toast({ title: "Success", description: `Added ${amount} coins` }) - } - } - - const handleRemoveCoins = async () => { - const data = await removeAmount(Math.abs(Number(amount)), "Manual removal") - if (data) { + } else if (numAmount < 0) { + await remove(Math.abs(numAmount), "Manual removal") setAmount(DEFAULT_AMOUNT) - toast({ title: "Success", description: `Removed ${amount} coins` }) } } @@ -84,14 +78,7 @@ export default function CoinsManager() { diff --git a/components/HabitList.tsx b/components/HabitList.tsx index 43cd463..abf70ba 100644 --- a/components/HabitList.tsx +++ b/components/HabitList.tsx @@ -1,17 +1,22 @@ 'use client' import { useState } from 'react' -import { useHabits } from '@/hooks/useHabits' import { Plus, ListTodo } from 'lucide-react' +import { useAtom } from 'jotai' +import { habitsAtom, settingsAtom } from '@/lib/atoms' import EmptyState from './EmptyState' import { Button } from '@/components/ui/button' import HabitItem from './HabitItem' import AddEditHabitModal from './AddEditHabitModal' import ConfirmDialog from './ConfirmDialog' import { Habit } from '@/lib/types' +import { useHabits } from '@/hooks/useHabits' export default function HabitList() { - const { habits, addHabit, editHabit, deleteHabit, completeHabit, undoComplete } = useHabits() + const { saveHabit, deleteHabit } = useHabits() + const [habitsData, setHabitsData] = useAtom(habitsAtom) + const habits = habitsData.habits + const [settings] = useAtom(settingsAtom) const [isModalOpen, setIsModalOpen] = useState(false) const [editingHabit, setEditingHabit] = useState(null) const [deleteConfirmation, setDeleteConfirmation] = useState<{ isOpen: boolean, habitId: string | null }>({ @@ -39,17 +44,15 @@ export default function HabitList() { ) : ( habits.map((habit) => ( - { - setEditingHabit(habit) - setIsModalOpen(true) - }} - onDelete={() => setDeleteConfirmation({ isOpen: true, habitId: habit.id })} - onComplete={() => completeHabit(habit)} - onUndo={() => undoComplete(habit)} - /> + { + setEditingHabit(habit) + setIsModalOpen(true) + }} + onDelete={() => setDeleteConfirmation({ isOpen: true, habitId: habit.id })} + /> )) )} @@ -60,11 +63,7 @@ export default function HabitList() { setEditingHabit(null) }} onSave={async (habit) => { - if (editingHabit) { - await editHabit({ ...habit, id: editingHabit.id }) - } else { - await addHabit(habit) - } + await saveHabit({ ...habit, id: editingHabit?.id }) setIsModalOpen(false) setEditingHabit(null) }} @@ -73,9 +72,9 @@ export default function HabitList() { setDeleteConfirmation({ isOpen: false, habitId: null })} - onConfirm={() => { + onConfirm={async () => { if (deleteConfirmation.habitId) { - deleteHabit(deleteConfirmation.habitId) + await deleteHabit(deleteConfirmation.habitId) } setDeleteConfirmation({ isOpen: false, habitId: null }) }} diff --git a/components/HabitOverview.tsx b/components/HabitOverview.tsx index 08d9396..d9af762 100644 --- a/components/HabitOverview.tsx +++ b/components/HabitOverview.tsx @@ -1,22 +1,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { BarChart } from 'lucide-react' -import { useEffect, useState } from 'react' import { getTodayInTimezone } from '@/lib/utils' -import { loadHabitsData } from '@/app/actions/data' -import { Habit } from '@/lib/types' import { useAtom } from 'jotai' -import { settingsAtom } from '@/lib/atoms' +import { habitsAtom, settingsAtom } from '@/lib/atoms' export default function HabitOverview() { - const [habits, setHabits] = useState([]) - - useEffect(() => { - const fetchHabits = async () => { - const data = await loadHabitsData() - setHabits(data.habits) - } - fetchHabits() - }, []) + const [habitsData] = useAtom(habitsAtom) + const habits = habitsData.habits const [settings] = useAtom(settingsAtom) const today = getTodayInTimezone(settings.system.timezone) diff --git a/components/WishlistManager.tsx b/components/WishlistManager.tsx index 2cc6268..9f8d56f 100644 --- a/components/WishlistManager.tsx +++ b/components/WishlistManager.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect, useRef } from 'react' +import { useWishlist } from '@/hooks/useWishlist' import { Plus, Gift } from 'lucide-react' import EmptyState from './EmptyState' import { Button } from '@/components/ui/button' @@ -8,16 +9,15 @@ import WishlistItem from './WishlistItem' import AddEditWishlistItemModal from './AddEditWishlistItemModal' import ConfirmDialog from './ConfirmDialog' import { WishlistItemType } from '@/lib/types' -import { useWishlist } from '@/hooks/useWishlist' export default function WishlistManager() { const { - wishlistItems, addWishlistItem, editWishlistItem, deleteWishlistItem, redeemWishlistItem, - canRedeem + canRedeem, + wishlistItems } = useWishlist() const [highlightedItemId, setHighlightedItemId] = useState(null) diff --git a/components/jotai-hydrate.tsx b/components/jotai-hydrate.tsx index d9e96cb..ddc4f82 100644 --- a/components/jotai-hydrate.tsx +++ b/components/jotai-hydrate.tsx @@ -1,16 +1,18 @@ 'use client' -import { settingsAtom } from "@/lib/atoms" +import { settingsAtom, habitsAtom, coinsAtom, wishlistAtom } from "@/lib/atoms" import { useHydrateAtoms } from "jotai/utils" -import { Settings } from "@/lib/types" +import { JotaiHydrateInitialValues } from "@/lib/types" -export function JotaiHydrate({ +export function JotaiHydrate({ children, - initialSettings -}: { - children: React.ReactNode - initialSettings: Settings -}) { - useHydrateAtoms([[settingsAtom, initialSettings]]) + initialValues +}: { children: React.ReactNode, initialValues: JotaiHydrateInitialValues }) { + useHydrateAtoms([ + [settingsAtom, initialValues.settings], + [habitsAtom, initialValues.habits], + [coinsAtom, initialValues.coins], + [wishlistAtom, initialValues.wishlist] + ]) return children } diff --git a/components/linkify.tsx b/components/linkify.tsx index 760da26..6dac658 100644 --- a/components/linkify.tsx +++ b/components/linkify.tsx @@ -1,5 +1,5 @@ import Linkify0 from "linkify-react"; export default function Linkify({ children }: { children: React.ReactNode }) { - return {children}; + return {children}; } diff --git a/hooks/useCoins.tsx b/hooks/useCoins.tsx index 2dc51da..803cefc 100644 --- a/hooks/useCoins.tsx +++ b/hooks/useCoins.tsx @@ -1,28 +1,12 @@ -import { useState, useEffect } from 'react' -import { loadCoinsData, addCoins, removeCoins } from '@/app/actions/data' +import { useAtom } from 'jotai' +import { coinsAtom } from '@/lib/atoms' +import { addCoins, removeCoins } from '@/app/actions/data' import { toast } from '@/hooks/use-toast' -import { CoinTransaction, TransactionType } from '@/lib/types' export function useCoins() { - const [balance, setBalance] = useState(0) - const [transactions, setTransactions] = useState([]) + const [coins, setCoins] = useAtom(coinsAtom) - useEffect(() => { - fetchCoins() - }, []) - - const fetchCoins = async () => { - const data = await loadCoinsData() - setBalance(data.balance) - setTransactions(data.transactions || []) - } - - const addAmount = async ( - amount: number, - description: string, - type: TransactionType = 'MANUAL_ADJUSTMENT', - relatedItemId?: string - ) => { + const add = async (amount: number, description: string) => { if (isNaN(amount) || amount <= 0) { toast({ title: "Invalid amount", @@ -31,19 +15,15 @@ export function useCoins() { return null } - const data = await addCoins(amount, description, type, relatedItemId) - setBalance(data.balance) - setTransactions(data.transactions) + const data = await addCoins(amount, description) + setCoins(data) + toast({ title: "Success", description: `Added ${amount} coins` }) return data } - const removeAmount = async ( - amount: number, - description: string, - type: TransactionType = 'MANUAL_ADJUSTMENT', - relatedItemId?: string - ) => { - if (isNaN(amount) || amount <= 0) { + const remove = async (amount: number, description: string) => { + const numAmount = Math.abs(amount) + if (isNaN(numAmount) || numAmount <= 0) { toast({ title: "Invalid amount", description: "Please enter a valid positive number" @@ -51,17 +31,16 @@ export function useCoins() { return null } - const data = await removeCoins(amount, description, type, relatedItemId) - setBalance(data.balance) - setTransactions(data.transactions) + const data = await removeCoins(numAmount, description) + setCoins(data) + toast({ title: "Success", description: `Removed ${numAmount} coins` }) return data } return { - balance, - transactions, - addAmount, - removeAmount, - fetchCoins + add, + remove, + balance: coins.balance, + transactions: coins.transactions } } diff --git a/hooks/useHabits.tsx b/hooks/useHabits.tsx index 565f6e7..f492ccd 100644 --- a/hooks/useHabits.tsx +++ b/hooks/useHabits.tsx @@ -1,69 +1,47 @@ -import { useState, useEffect } from 'react' -import { loadHabitsData, saveHabitsData, addCoins, removeCoins } from '@/app/actions/data' +import { useAtom } from 'jotai' +import { habitsAtom, coinsAtom, settingsAtom } from '@/lib/atoms' +import { addCoins, removeCoins, saveHabitsData } from '@/app/actions/data' +import { Habit } from '@/lib/types' +import { getNowInMilliseconds, getTodayInTimezone } from '@/lib/utils' import { toast } from '@/hooks/use-toast' import { ToastAction } from '@/components/ui/toast' import { Undo2 } from 'lucide-react' -import { Habit } from '@/lib/types' -import { getNowInMilliseconds, getTodayInTimezone } from '@/lib/utils' -import { useAtom } from 'jotai' -import { settingsAtom } from '@/lib/atoms' export function useHabits() { - const [habits, setHabits] = useState([]) + const [habitsData, setHabitsData] = useAtom(habitsAtom) + const [coins, setCoins] = useAtom(coinsAtom) const [settings] = useAtom(settingsAtom) - useEffect(() => { - fetchHabits() - }, []) - - const fetchHabits = async () => { - const data = await loadHabitsData() - setHabits(data.habits) - } - - const addHabit = async (habit: Omit) => { - const newHabit = { ...habit, id: getNowInMilliseconds() } - const newHabits = [...habits, newHabit] - setHabits(newHabits) - await saveHabitsData({ habits: newHabits }) - } - - const editHabit = async (updatedHabit: Habit) => { - const newHabits = habits.map(habit => - habit.id === updatedHabit.id ? updatedHabit : habit - ) - setHabits(newHabits) - await saveHabitsData({ habits: newHabits }) - } - - const deleteHabit = async (id: string) => { - const newHabits = habits.filter(habit => habit.id !== id) - setHabits(newHabits) - await saveHabitsData({ habits: newHabits }) - } - const completeHabit = async (habit: Habit) => { - const today = getTodayInTimezone(settings.system.timezone) + const timezone = settings.system.timezone + const today = getTodayInTimezone(timezone) if (!habit.completions.includes(today)) { const updatedHabit = { ...habit, completions: [...habit.completions, today] } - const updatedHabits = habits.map(h => + const updatedHabits = habitsData.habits.map(h => h.id === habit.id ? updatedHabit : h ) await saveHabitsData({ habits: updatedHabits }) - const coinsData = await addCoins(habit.coinReward, `Completed habit: ${habit.name}`, 'HABIT_COMPLETION', habit.id) + setHabitsData({ habits: updatedHabits }) - setHabits(updatedHabits) + const coinsData = await addCoins(habit.coinReward, `Completed habit: ${habit.name}`, 'HABIT_COMPLETION', habit.id) + setCoins(coinsData) toast({ title: "Habit completed!", description: `You earned ${habit.coinReward} coins.`, - action: undoComplete(habit)}>Undo + action: undoComplete(habit)}> + Undo + }) - return coinsData.balance + return { + updatedHabits, + newBalance: coinsData.balance, + newTransactions: coinsData.transactions + } } else { toast({ title: "Habit already completed", @@ -75,34 +53,61 @@ export function useHabits() { } const undoComplete = async (habit: Habit) => { - const today = getTodayInTimezone(settings.system.timezone) + const timezone = settings.system.timezone + const today = getTodayInTimezone(timezone) const updatedHabit = { ...habit, completions: habit.completions.filter(date => date !== today) } - const updatedHabits = habits.map(h => + const updatedHabits = habitsData.habits.map(h => h.id === habit.id ? updatedHabit : h ) await saveHabitsData({ habits: updatedHabits }) - const coinsData = await removeCoins(habit.coinReward, `Undid habit completion: ${habit.name}`, 'HABIT_UNDO', habit.id) + setHabitsData({ habits: updatedHabits }) - setHabits(updatedHabits) + const coinsData = await removeCoins(habit.coinReward, `Undid habit completion: ${habit.name}`, 'HABIT_UNDO', habit.id) + setCoins(coinsData) toast({ title: "Completion undone", description: `${habit.coinReward} coins have been deducted.`, - action: completeHabit(habit)}>Undo + action: completeHabit(habit)}> + Undo + }) - return coinsData.balance + return { + updatedHabits, + newBalance: coinsData.balance, + newTransactions: coinsData.transactions + } + } + + const saveHabit = async (habit: Omit & { id?: string }) => { + const newHabit = { + ...habit, + id: habit.id || getNowInMilliseconds().toString() + } + const updatedHabits = habit.id + ? habitsData.habits.map(h => h.id === habit.id ? newHabit : h) + : [...habitsData.habits, newHabit] + + await saveHabitsData({ habits: updatedHabits }) + setHabitsData({ habits: updatedHabits }) + return updatedHabits + } + + const deleteHabit = async (id: string) => { + const updatedHabits = habitsData.habits.filter(h => h.id !== id) + await saveHabitsData({ habits: updatedHabits }) + setHabitsData({ habits: updatedHabits }) + return updatedHabits } return { - habits, - addHabit, - editHabit, - deleteHabit, completeHabit, - undoComplete + undoComplete, + saveHabit, + deleteHabit } } diff --git a/hooks/useWishlist.tsx b/hooks/useWishlist.tsx index a267222..6595b0a 100644 --- a/hooks/useWishlist.tsx +++ b/hooks/useWishlist.tsx @@ -1,52 +1,45 @@ -import { useState, useEffect } from 'react' -import { loadWishlistItems, saveWishlistItems } from '@/app/actions/data' +import { useAtom } from 'jotai' +import { wishlistAtom, coinsAtom } from '@/lib/atoms' +import { saveWishlistItems, removeCoins } from '@/app/actions/data' import { toast } from '@/hooks/use-toast' import { WishlistItemType } from '@/lib/types' -import { useCoins } from '@/hooks/useCoins' import { celebrations } from '@/utils/celebrations' export function useWishlist() { - const [wishlistItems, setWishlistItems] = useState([]) - const { balance, removeAmount } = useCoins() - - useEffect(() => { - fetchWishlistItems() - }, []) - - const fetchWishlistItems = async () => { - const items = await loadWishlistItems() - setWishlistItems(items) - } + const [wishlist, setWishlist] = useAtom(wishlistAtom) + const [coins, setCoins] = useAtom(coinsAtom) + const balance = coins.balance const addWishlistItem = async (item: Omit) => { const newItem = { ...item, id: Date.now().toString() } - const newItems = [...wishlistItems, newItem] - setWishlistItems(newItems) + const newItems = [...wishlist.items, newItem] + setWishlist({ items: newItems }) await saveWishlistItems(newItems) } const editWishlistItem = async (updatedItem: WishlistItemType) => { - const newItems = wishlistItems.map(item => + const newItems = wishlist.items.map(item => item.id === updatedItem.id ? updatedItem : item ) - setWishlistItems(newItems) + setWishlist({ items: newItems }) await saveWishlistItems(newItems) } const deleteWishlistItem = async (id: string) => { - const newItems = wishlistItems.filter(item => item.id !== id) - setWishlistItems(newItems) + const newItems = wishlist.items.filter(item => item.id !== id) + setWishlist({ items: newItems }) await saveWishlistItems(newItems) } const redeemWishlistItem = async (item: WishlistItemType) => { if (balance >= item.coinCost) { - await removeAmount( + const data = await removeCoins( item.coinCost, `Redeemed reward: ${item.name}`, 'WISH_REDEMPTION', item.id ) + setCoins(data) // Randomly choose a celebration effect const celebrationEffects = [ @@ -71,12 +64,14 @@ export function useWishlist() { } } + const canRedeem = (cost: number) => balance >= cost + return { - wishlistItems, addWishlistItem, editWishlistItem, deleteWishlistItem, redeemWishlistItem, - canRedeem: (cost: number) => balance >= cost + canRedeem, + wishlistItems: wishlist.items } } diff --git a/lib/atoms.ts b/lib/atoms.ts index ad01145..14f89b7 100644 --- a/lib/atoms.ts +++ b/lib/atoms.ts @@ -1,4 +1,12 @@ import { atom } from "jotai"; -import { getDefaultSettings } from "./types"; +import { + getDefaultSettings, + getDefaultHabitsData, + getDefaultCoinsData, + getDefaultWishlistData +} from "./types"; -export const settingsAtom = atom(getDefaultSettings()) +export const settingsAtom = atom(getDefaultSettings()); +export const habitsAtom = atom(getDefaultHabitsData()); +export const coinsAtom = atom(getDefaultCoinsData()); +export const wishlistAtom = atom(getDefaultWishlistData()); diff --git a/lib/types.ts b/lib/types.ts index dbf4e43..a1746b8 100644 --- a/lib/types.ts +++ b/lib/types.ts @@ -88,3 +88,10 @@ export interface Settings { ui: UISettings; system: SystemSettings; } + +export interface JotaiHydrateInitialValues { + settings: Settings; + coins: CoinsData; + habits: HabitsData; + wishlist: WishlistData; +} diff --git a/package.json b/package.json index c30ab65..8b2b332 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "habittrove", - "version": "0.1.7", + "version": "0.1.8", "private": true, "scripts": { "dev": "next dev --turbopack",