mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
use jotai for all states (#19)
This commit is contained in:
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Version 0.1.8
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- use jotai for all state management
|
||||||
|
|
||||||
## Version 0.1.7
|
## Version 0.1.7
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
DATA_DEFAULTS,
|
DATA_DEFAULTS,
|
||||||
getDefaultSettings
|
getDefaultSettings
|
||||||
} from '@/lib/types'
|
} from '@/lib/types'
|
||||||
import { d2t, getNow, getNowInMilliseconds } from '@/lib/utils';
|
import { d2t, getNow } from '@/lib/utils';
|
||||||
|
|
||||||
function getDefaultData<T>(type: DataType): T {
|
function getDefaultData<T>(type: DataType): T {
|
||||||
return DATA_DEFAULTS[type]() as T;
|
return DATA_DEFAULTS[type]() as T;
|
||||||
@@ -65,8 +65,12 @@ async function saveData<T>(type: DataType, data: T): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wishlist specific functions
|
// Wishlist specific functions
|
||||||
|
export async function loadWishlistData(): Promise<WishlistData> {
|
||||||
|
return loadData<WishlistData>('wishlist')
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadWishlistItems(): Promise<WishlistItemType[]> {
|
export async function loadWishlistItems(): Promise<WishlistItemType[]> {
|
||||||
const data = await loadData<WishlistData>('wishlist')
|
const data = await loadWishlistData()
|
||||||
return data.items
|
return data.items
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { Toaster } from '@/components/ui/toaster'
|
|||||||
import { JotaiProvider } from '@/components/jotai-providers'
|
import { JotaiProvider } from '@/components/jotai-providers'
|
||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
import { JotaiHydrate } from '@/components/jotai-hydrate'
|
import { JotaiHydrate } from '@/components/jotai-hydrate'
|
||||||
import { loadSettings } from './actions/data'
|
import { loadSettings, loadHabitsData, loadCoinsData, loadWishlistData } from './actions/data'
|
||||||
// Inter (clean, modern, excellent readability)
|
// Inter (clean, modern, excellent readability)
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@@ -32,13 +32,26 @@ export default async function RootLayout({
|
|||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const initialSettings = await loadSettings()
|
const [initialSettings, initialHabits, initialCoins, initialWishlist] = await Promise.all([
|
||||||
|
loadSettings(),
|
||||||
|
loadHabitsData(),
|
||||||
|
loadCoinsData(),
|
||||||
|
loadWishlistData()
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={activeFont.className}>
|
<body className={activeFont.className}>
|
||||||
<JotaiProvider>
|
<JotaiProvider>
|
||||||
<Suspense fallback="loading">
|
<Suspense fallback="loading">
|
||||||
<JotaiHydrate initialSettings={initialSettings}>
|
<JotaiHydrate
|
||||||
|
initialValues={{
|
||||||
|
settings: initialSettings,
|
||||||
|
habits: initialHabits,
|
||||||
|
coins: initialCoins,
|
||||||
|
wishlist: initialWishlist
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</JotaiHydrate>
|
</JotaiHydrate>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
|
|||||||
@@ -8,31 +8,25 @@ import { History } from 'lucide-react'
|
|||||||
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 { toast } from '@/hooks/use-toast'
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
import { useCoins } from '@/hooks/useCoins'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { settingsAtom } from '@/lib/atoms'
|
import { useCoins } from '@/hooks/useCoins'
|
||||||
|
|
||||||
export default function CoinsManager() {
|
export default function CoinsManager() {
|
||||||
const { balance, transactions, addAmount, removeAmount } = useCoins()
|
const { add, remove, balance, transactions } = useCoins()
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const DEFAULT_AMOUNT = '0'
|
const DEFAULT_AMOUNT = '0'
|
||||||
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
||||||
|
|
||||||
const handleAddCoins = async () => {
|
const handleAddRemoveCoins = async () => {
|
||||||
const data = await addAmount(Number(amount), "Manual addition")
|
const numAmount = Number(amount)
|
||||||
if (data) {
|
if (numAmount > 0) {
|
||||||
|
await add(numAmount, "Manual addition")
|
||||||
setAmount(DEFAULT_AMOUNT)
|
setAmount(DEFAULT_AMOUNT)
|
||||||
toast({ title: "Success", description: `Added ${amount} coins` })
|
} else if (numAmount < 0) {
|
||||||
}
|
await remove(Math.abs(numAmount), "Manual removal")
|
||||||
}
|
|
||||||
|
|
||||||
const handleRemoveCoins = async () => {
|
|
||||||
const data = await removeAmount(Math.abs(Number(amount)), "Manual removal")
|
|
||||||
if (data) {
|
|
||||||
setAmount(DEFAULT_AMOUNT)
|
setAmount(DEFAULT_AMOUNT)
|
||||||
toast({ title: "Success", description: `Removed ${amount} coins` })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,14 +78,7 @@ export default function CoinsManager() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={handleAddRemoveCoins}
|
||||||
const numAmount = Number(amount);
|
|
||||||
if (numAmount > 0) {
|
|
||||||
handleAddCoins();
|
|
||||||
} else if (numAmount < 0) {
|
|
||||||
handleRemoveCoins();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-full h-14 transition-colors flex items-center justify-center font-medium"
|
className="w-full h-14 transition-colors flex items-center justify-center font-medium"
|
||||||
variant="default"
|
variant="default"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { loadCoinsData } from '@/app/actions/data'
|
import { useAtom } from 'jotai'
|
||||||
import { useHabits } from '@/hooks/useHabits'
|
import { wishlistAtom, habitsAtom, settingsAtom, coinsAtom } from '@/lib/atoms'
|
||||||
import { useWishlist } from '@/hooks/useWishlist'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import CoinBalance from './CoinBalance'
|
import CoinBalance from './CoinBalance'
|
||||||
import DailyOverview from './DailyOverview'
|
import DailyOverview from './DailyOverview'
|
||||||
import HabitOverview from './HabitOverview'
|
|
||||||
import HabitStreak from './HabitStreak'
|
import HabitStreak from './HabitStreak'
|
||||||
|
import { useHabits } from '@/hooks/useHabits'
|
||||||
|
|
||||||
export default function Dashboard() {
|
export default function Dashboard() {
|
||||||
const { habits, completeHabit, undoComplete } = useHabits()
|
const { completeHabit, undoComplete } = useHabits()
|
||||||
const [coinBalance, setCoinBalance] = useState(0)
|
const [habitsData] = useAtom(habitsAtom)
|
||||||
const { wishlistItems } = useWishlist()
|
const habits = habitsData.habits
|
||||||
|
const [settings] = useAtom(settingsAtom)
|
||||||
useEffect(() => {
|
const [coins] = useAtom(coinsAtom)
|
||||||
const loadData = async () => {
|
const coinBalance = coins.balance
|
||||||
const coinsData = await loadCoinsData()
|
const [wishlist] = useAtom(wishlistAtom)
|
||||||
setCoinBalance(coinsData.balance)
|
const wishlistItems = wishlist.items
|
||||||
}
|
|
||||||
loadData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
@@ -33,18 +28,8 @@ export default function Dashboard() {
|
|||||||
wishlistItems={wishlistItems}
|
wishlistItems={wishlistItems}
|
||||||
habits={habits}
|
habits={habits}
|
||||||
coinBalance={coinBalance}
|
coinBalance={coinBalance}
|
||||||
onComplete={async (habit) => {
|
onComplete={completeHabit}
|
||||||
const newBalance = await completeHabit(habit)
|
onUndo={undoComplete}
|
||||||
if (newBalance !== null) {
|
|
||||||
setCoinBalance(newBalance)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onUndo={async (habit) => {
|
|
||||||
const newBalance = await undoComplete(habit)
|
|
||||||
if (newBalance !== null) {
|
|
||||||
setCoinBalance(newBalance)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* <HabitHeatmap habits={habits} /> */}
|
{/* <HabitHeatmap habits={habits} /> */}
|
||||||
|
|||||||
@@ -1,31 +1,20 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Calendar } from '@/components/ui/calendar'
|
import { Calendar } from '@/components/ui/calendar'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
|
|
||||||
import { loadHabitsData } from '@/app/actions/data'
|
|
||||||
import { Habit } from '@/lib/types'
|
|
||||||
import { d2s, getNow } from '@/lib/utils'
|
import { d2s, getNow } from '@/lib/utils'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { settingsAtom } from '@/lib/atoms'
|
import { habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import Linkify from './linkify'
|
import Linkify from './linkify'
|
||||||
|
|
||||||
export default function HabitCalendar() {
|
export default function HabitCalendar() {
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const [selectedDate, setSelectedDate] = useState<DateTime>(getNow({ timezone: settings.system.timezone }))
|
const [selectedDate, setSelectedDate] = useState<DateTime>(getNow({ timezone: settings.system.timezone }))
|
||||||
const [habits, setHabits] = useState<Habit[]>([])
|
const [habitsData] = useAtom(habitsAtom)
|
||||||
|
const habits = habitsData.habits
|
||||||
useEffect(() => {
|
|
||||||
fetchHabitsData()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const fetchHabitsData = async () => {
|
|
||||||
const data = await loadHabitsData()
|
|
||||||
setHabits(data.habits)
|
|
||||||
}
|
|
||||||
|
|
||||||
const getHabitsForDate = (date: Date) => {
|
const getHabitsForDate = (date: Date) => {
|
||||||
const dateString = date.toISOString().split('T')[0]
|
const dateString = date.toISOString().split('T')[0]
|
||||||
@@ -46,7 +35,7 @@ export default function HabitCalendar() {
|
|||||||
<Calendar
|
<Calendar
|
||||||
mode="single"
|
mode="single"
|
||||||
selected={selectedDate.toJSDate()}
|
selected={selectedDate.toJSDate()}
|
||||||
// onSelect={(e) => e && setSelectedDate(DateTime.fromJSDate(e))}
|
onSelect={(e) => e && setSelectedDate(DateTime.fromJSDate(e))}
|
||||||
className="rounded-md border"
|
className="rounded-md border"
|
||||||
modifiers={{
|
modifiers={{
|
||||||
completed: (date) => getHabitsForDate(date).length > 0,
|
completed: (date) => getHabitsForDate(date).length > 0,
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Coins, Edit, Trash2, Check, Undo2 } from 'lucide-react'
|
import { Coins, Edit, Trash2, Check, Undo2 } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useHabits } from '@/hooks/useHabits'
|
||||||
|
|
||||||
interface HabitItemProps {
|
interface HabitItemProps {
|
||||||
habit: Habit
|
habit: Habit
|
||||||
onEdit: () => void
|
onEdit: () => void
|
||||||
onDelete: () => void
|
onDelete: () => void
|
||||||
onComplete: () => void
|
|
||||||
onUndo: () => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HabitItem({ habit, onEdit, onDelete, onComplete, onUndo }: HabitItemProps) {
|
export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||||
|
const { completeHabit, undoComplete } = useHabits()
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const today = getTodayInTimezone(settings.system.timezone)
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
const isCompletedToday = habit.completions?.includes(today)
|
const isCompletedToday = habit.completions?.includes(today)
|
||||||
@@ -71,7 +71,7 @@ export default function HabitItem({ habit, onEdit, onDelete, onComplete, onUndo
|
|||||||
<Button
|
<Button
|
||||||
variant={isCompletedToday ? "secondary" : "default"}
|
variant={isCompletedToday ? "secondary" : "default"}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onComplete}
|
onClick={async () => await completeHabit(habit)}
|
||||||
disabled={isCompletedToday}
|
disabled={isCompletedToday}
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4 mr-2" />
|
<Check className="h-4 w-4 mr-2" />
|
||||||
@@ -81,7 +81,7 @@ export default function HabitItem({ habit, onEdit, onDelete, onComplete, onUndo
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={onUndo}
|
onClick={async () => await undoComplete(habit)}
|
||||||
>
|
>
|
||||||
<Undo2 />
|
<Undo2 />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,17 +1,22 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useHabits } from '@/hooks/useHabits'
|
|
||||||
import { Plus, ListTodo } from 'lucide-react'
|
import { Plus, ListTodo } from 'lucide-react'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import { habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||||
import EmptyState from './EmptyState'
|
import EmptyState from './EmptyState'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import HabitItem from './HabitItem'
|
import HabitItem from './HabitItem'
|
||||||
import AddEditHabitModal from './AddEditHabitModal'
|
import AddEditHabitModal from './AddEditHabitModal'
|
||||||
import ConfirmDialog from './ConfirmDialog'
|
import ConfirmDialog from './ConfirmDialog'
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
|
import { useHabits } from '@/hooks/useHabits'
|
||||||
|
|
||||||
export default function HabitList() {
|
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 [isModalOpen, setIsModalOpen] = useState(false)
|
||||||
const [editingHabit, setEditingHabit] = useState<Habit | null>(null)
|
const [editingHabit, setEditingHabit] = useState<Habit | null>(null)
|
||||||
const [deleteConfirmation, setDeleteConfirmation] = useState<{ isOpen: boolean, habitId: string | null }>({
|
const [deleteConfirmation, setDeleteConfirmation] = useState<{ isOpen: boolean, habitId: string | null }>({
|
||||||
@@ -39,17 +44,15 @@ export default function HabitList() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
habits.map((habit) => (
|
habits.map((habit) => (
|
||||||
<HabitItem
|
<HabitItem
|
||||||
key={habit.id}
|
key={habit.id}
|
||||||
habit={habit}
|
habit={habit}
|
||||||
onEdit={() => {
|
onEdit={() => {
|
||||||
setEditingHabit(habit)
|
setEditingHabit(habit)
|
||||||
setIsModalOpen(true)
|
setIsModalOpen(true)
|
||||||
}}
|
}}
|
||||||
onDelete={() => setDeleteConfirmation({ isOpen: true, habitId: habit.id })}
|
onDelete={() => setDeleteConfirmation({ isOpen: true, habitId: habit.id })}
|
||||||
onComplete={() => completeHabit(habit)}
|
/>
|
||||||
onUndo={() => undoComplete(habit)}
|
|
||||||
/>
|
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -60,11 +63,7 @@ export default function HabitList() {
|
|||||||
setEditingHabit(null)
|
setEditingHabit(null)
|
||||||
}}
|
}}
|
||||||
onSave={async (habit) => {
|
onSave={async (habit) => {
|
||||||
if (editingHabit) {
|
await saveHabit({ ...habit, id: editingHabit?.id })
|
||||||
await editHabit({ ...habit, id: editingHabit.id })
|
|
||||||
} else {
|
|
||||||
await addHabit(habit)
|
|
||||||
}
|
|
||||||
setIsModalOpen(false)
|
setIsModalOpen(false)
|
||||||
setEditingHabit(null)
|
setEditingHabit(null)
|
||||||
}}
|
}}
|
||||||
@@ -73,9 +72,9 @@ export default function HabitList() {
|
|||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
isOpen={deleteConfirmation.isOpen}
|
isOpen={deleteConfirmation.isOpen}
|
||||||
onClose={() => setDeleteConfirmation({ isOpen: false, habitId: null })}
|
onClose={() => setDeleteConfirmation({ isOpen: false, habitId: null })}
|
||||||
onConfirm={() => {
|
onConfirm={async () => {
|
||||||
if (deleteConfirmation.habitId) {
|
if (deleteConfirmation.habitId) {
|
||||||
deleteHabit(deleteConfirmation.habitId)
|
await deleteHabit(deleteConfirmation.habitId)
|
||||||
}
|
}
|
||||||
setDeleteConfirmation({ isOpen: false, habitId: null })
|
setDeleteConfirmation({ isOpen: false, habitId: null })
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -1,22 +1,12 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { BarChart } from 'lucide-react'
|
import { BarChart } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { getTodayInTimezone } from '@/lib/utils'
|
import { getTodayInTimezone } from '@/lib/utils'
|
||||||
import { loadHabitsData } from '@/app/actions/data'
|
|
||||||
import { Habit } from '@/lib/types'
|
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { settingsAtom } from '@/lib/atoms'
|
import { habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||||
|
|
||||||
export default function HabitOverview() {
|
export default function HabitOverview() {
|
||||||
const [habits, setHabits] = useState<Habit[]>([])
|
const [habitsData] = useAtom(habitsAtom)
|
||||||
|
const habits = habitsData.habits
|
||||||
useEffect(() => {
|
|
||||||
const fetchHabits = async () => {
|
|
||||||
const data = await loadHabitsData()
|
|
||||||
setHabits(data.habits)
|
|
||||||
}
|
|
||||||
fetchHabits()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const today = getTodayInTimezone(settings.system.timezone)
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
|
import { useWishlist } from '@/hooks/useWishlist'
|
||||||
import { Plus, Gift } from 'lucide-react'
|
import { Plus, Gift } from 'lucide-react'
|
||||||
import EmptyState from './EmptyState'
|
import EmptyState from './EmptyState'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -8,16 +9,15 @@ import WishlistItem from './WishlistItem'
|
|||||||
import AddEditWishlistItemModal from './AddEditWishlistItemModal'
|
import AddEditWishlistItemModal from './AddEditWishlistItemModal'
|
||||||
import ConfirmDialog from './ConfirmDialog'
|
import ConfirmDialog from './ConfirmDialog'
|
||||||
import { WishlistItemType } from '@/lib/types'
|
import { WishlistItemType } from '@/lib/types'
|
||||||
import { useWishlist } from '@/hooks/useWishlist'
|
|
||||||
|
|
||||||
export default function WishlistManager() {
|
export default function WishlistManager() {
|
||||||
const {
|
const {
|
||||||
wishlistItems,
|
|
||||||
addWishlistItem,
|
addWishlistItem,
|
||||||
editWishlistItem,
|
editWishlistItem,
|
||||||
deleteWishlistItem,
|
deleteWishlistItem,
|
||||||
redeemWishlistItem,
|
redeemWishlistItem,
|
||||||
canRedeem
|
canRedeem,
|
||||||
|
wishlistItems
|
||||||
} = useWishlist()
|
} = useWishlist()
|
||||||
|
|
||||||
const [highlightedItemId, setHighlightedItemId] = useState<string | null>(null)
|
const [highlightedItemId, setHighlightedItemId] = useState<string | null>(null)
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { settingsAtom } from "@/lib/atoms"
|
import { settingsAtom, habitsAtom, coinsAtom, wishlistAtom } from "@/lib/atoms"
|
||||||
import { useHydrateAtoms } from "jotai/utils"
|
import { useHydrateAtoms } from "jotai/utils"
|
||||||
import { Settings } from "@/lib/types"
|
import { JotaiHydrateInitialValues } from "@/lib/types"
|
||||||
|
|
||||||
export function JotaiHydrate({
|
export function JotaiHydrate({
|
||||||
children,
|
children,
|
||||||
initialSettings
|
initialValues
|
||||||
}: {
|
}: { children: React.ReactNode, initialValues: JotaiHydrateInitialValues }) {
|
||||||
children: React.ReactNode
|
useHydrateAtoms([
|
||||||
initialSettings: Settings
|
[settingsAtom, initialValues.settings],
|
||||||
}) {
|
[habitsAtom, initialValues.habits],
|
||||||
useHydrateAtoms([[settingsAtom, initialSettings]])
|
[coinsAtom, initialValues.coins],
|
||||||
|
[wishlistAtom, initialValues.wishlist]
|
||||||
|
])
|
||||||
return children
|
return children
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Linkify0 from "linkify-react";
|
import Linkify0 from "linkify-react";
|
||||||
|
|
||||||
export default function Linkify({ children }: { children: React.ReactNode }) {
|
export default function Linkify({ children }: { children: React.ReactNode }) {
|
||||||
return <Linkify0 options={{ className: "underline" }}>{children}</Linkify0>;
|
return <Linkify0 options={{ className: "underline", target: "_blank" }}>{children}</Linkify0>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,12 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useAtom } from 'jotai'
|
||||||
import { loadCoinsData, addCoins, removeCoins } from '@/app/actions/data'
|
import { coinsAtom } from '@/lib/atoms'
|
||||||
|
import { addCoins, removeCoins } from '@/app/actions/data'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { CoinTransaction, TransactionType } from '@/lib/types'
|
|
||||||
|
|
||||||
export function useCoins() {
|
export function useCoins() {
|
||||||
const [balance, setBalance] = useState(0)
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
const [transactions, setTransactions] = useState<CoinTransaction[]>([])
|
|
||||||
|
|
||||||
useEffect(() => {
|
const add = async (amount: number, description: string) => {
|
||||||
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
|
|
||||||
) => {
|
|
||||||
if (isNaN(amount) || amount <= 0) {
|
if (isNaN(amount) || amount <= 0) {
|
||||||
toast({
|
toast({
|
||||||
title: "Invalid amount",
|
title: "Invalid amount",
|
||||||
@@ -31,19 +15,15 @@ export function useCoins() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await addCoins(amount, description, type, relatedItemId)
|
const data = await addCoins(amount, description)
|
||||||
setBalance(data.balance)
|
setCoins(data)
|
||||||
setTransactions(data.transactions)
|
toast({ title: "Success", description: `Added ${amount} coins` })
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeAmount = async (
|
const remove = async (amount: number, description: string) => {
|
||||||
amount: number,
|
const numAmount = Math.abs(amount)
|
||||||
description: string,
|
if (isNaN(numAmount) || numAmount <= 0) {
|
||||||
type: TransactionType = 'MANUAL_ADJUSTMENT',
|
|
||||||
relatedItemId?: string
|
|
||||||
) => {
|
|
||||||
if (isNaN(amount) || amount <= 0) {
|
|
||||||
toast({
|
toast({
|
||||||
title: "Invalid amount",
|
title: "Invalid amount",
|
||||||
description: "Please enter a valid positive number"
|
description: "Please enter a valid positive number"
|
||||||
@@ -51,17 +31,16 @@ export function useCoins() {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await removeCoins(amount, description, type, relatedItemId)
|
const data = await removeCoins(numAmount, description)
|
||||||
setBalance(data.balance)
|
setCoins(data)
|
||||||
setTransactions(data.transactions)
|
toast({ title: "Success", description: `Removed ${numAmount} coins` })
|
||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
balance,
|
add,
|
||||||
transactions,
|
remove,
|
||||||
addAmount,
|
balance: coins.balance,
|
||||||
removeAmount,
|
transactions: coins.transactions
|
||||||
fetchCoins
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,69 +1,47 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useAtom } from 'jotai'
|
||||||
import { loadHabitsData, saveHabitsData, addCoins, removeCoins } from '@/app/actions/data'
|
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 { toast } from '@/hooks/use-toast'
|
||||||
import { ToastAction } from '@/components/ui/toast'
|
import { ToastAction } from '@/components/ui/toast'
|
||||||
import { Undo2 } from 'lucide-react'
|
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() {
|
export function useHabits() {
|
||||||
const [habits, setHabits] = useState<Habit[]>([])
|
const [habitsData, setHabitsData] = useAtom(habitsAtom)
|
||||||
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchHabits()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const fetchHabits = async () => {
|
|
||||||
const data = await loadHabitsData()
|
|
||||||
setHabits(data.habits)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addHabit = async (habit: Omit<Habit, 'id'>) => {
|
|
||||||
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 completeHabit = async (habit: Habit) => {
|
||||||
const today = getTodayInTimezone(settings.system.timezone)
|
const timezone = settings.system.timezone
|
||||||
|
const today = getTodayInTimezone(timezone)
|
||||||
if (!habit.completions.includes(today)) {
|
if (!habit.completions.includes(today)) {
|
||||||
const updatedHabit = {
|
const updatedHabit = {
|
||||||
...habit,
|
...habit,
|
||||||
completions: [...habit.completions, today]
|
completions: [...habit.completions, today]
|
||||||
}
|
}
|
||||||
const updatedHabits = habits.map(h =>
|
const updatedHabits = habitsData.habits.map(h =>
|
||||||
h.id === habit.id ? updatedHabit : h
|
h.id === habit.id ? updatedHabit : h
|
||||||
)
|
)
|
||||||
await saveHabitsData({ habits: updatedHabits })
|
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({
|
toast({
|
||||||
title: "Habit completed!",
|
title: "Habit completed!",
|
||||||
description: `You earned ${habit.coinReward} coins.`,
|
description: `You earned ${habit.coinReward} coins.`,
|
||||||
action: <ToastAction altText="Undo" className="gap-2" onClick={() => undoComplete(habit)}><Undo2 className="h-4 w-4" />Undo</ToastAction>
|
action: <ToastAction altText="Undo" className="gap-2" onClick={() => undoComplete(habit)}>
|
||||||
|
<Undo2 className="h-4 w-4" />Undo
|
||||||
|
</ToastAction>
|
||||||
})
|
})
|
||||||
|
|
||||||
return coinsData.balance
|
return {
|
||||||
|
updatedHabits,
|
||||||
|
newBalance: coinsData.balance,
|
||||||
|
newTransactions: coinsData.transactions
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast({
|
toast({
|
||||||
title: "Habit already completed",
|
title: "Habit already completed",
|
||||||
@@ -75,34 +53,61 @@ export function useHabits() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const undoComplete = async (habit: Habit) => {
|
const undoComplete = async (habit: Habit) => {
|
||||||
const today = getTodayInTimezone(settings.system.timezone)
|
const timezone = settings.system.timezone
|
||||||
|
const today = getTodayInTimezone(timezone)
|
||||||
const updatedHabit = {
|
const updatedHabit = {
|
||||||
...habit,
|
...habit,
|
||||||
completions: habit.completions.filter(date => date !== today)
|
completions: habit.completions.filter(date => date !== today)
|
||||||
}
|
}
|
||||||
const updatedHabits = habits.map(h =>
|
const updatedHabits = habitsData.habits.map(h =>
|
||||||
h.id === habit.id ? updatedHabit : h
|
h.id === habit.id ? updatedHabit : h
|
||||||
)
|
)
|
||||||
await saveHabitsData({ habits: updatedHabits })
|
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({
|
toast({
|
||||||
title: "Completion undone",
|
title: "Completion undone",
|
||||||
description: `${habit.coinReward} coins have been deducted.`,
|
description: `${habit.coinReward} coins have been deducted.`,
|
||||||
action: <ToastAction altText="Redo" onClick={() => completeHabit(habit)}><Undo2 className="h-4 w-4" />Undo</ToastAction>
|
action: <ToastAction altText="Redo" onClick={() => completeHabit(habit)}>
|
||||||
|
<Undo2 className="h-4 w-4" />Undo
|
||||||
|
</ToastAction>
|
||||||
})
|
})
|
||||||
|
|
||||||
return coinsData.balance
|
return {
|
||||||
|
updatedHabits,
|
||||||
|
newBalance: coinsData.balance,
|
||||||
|
newTransactions: coinsData.transactions
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveHabit = async (habit: Omit<Habit, 'id'> & { 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 {
|
return {
|
||||||
habits,
|
|
||||||
addHabit,
|
|
||||||
editHabit,
|
|
||||||
deleteHabit,
|
|
||||||
completeHabit,
|
completeHabit,
|
||||||
undoComplete
|
undoComplete,
|
||||||
|
saveHabit,
|
||||||
|
deleteHabit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +1,45 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useAtom } from 'jotai'
|
||||||
import { loadWishlistItems, saveWishlistItems } from '@/app/actions/data'
|
import { wishlistAtom, coinsAtom } from '@/lib/atoms'
|
||||||
|
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 } from '@/lib/types'
|
||||||
import { useCoins } from '@/hooks/useCoins'
|
|
||||||
import { celebrations } from '@/utils/celebrations'
|
import { celebrations } from '@/utils/celebrations'
|
||||||
|
|
||||||
export function useWishlist() {
|
export function useWishlist() {
|
||||||
const [wishlistItems, setWishlistItems] = useState<WishlistItemType[]>([])
|
const [wishlist, setWishlist] = useAtom(wishlistAtom)
|
||||||
const { balance, removeAmount } = useCoins()
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
|
const balance = coins.balance
|
||||||
useEffect(() => {
|
|
||||||
fetchWishlistItems()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const fetchWishlistItems = async () => {
|
|
||||||
const items = await loadWishlistItems()
|
|
||||||
setWishlistItems(items)
|
|
||||||
}
|
|
||||||
|
|
||||||
const addWishlistItem = async (item: Omit<WishlistItemType, 'id'>) => {
|
const addWishlistItem = async (item: Omit<WishlistItemType, 'id'>) => {
|
||||||
const newItem = { ...item, id: Date.now().toString() }
|
const newItem = { ...item, id: Date.now().toString() }
|
||||||
const newItems = [...wishlistItems, newItem]
|
const newItems = [...wishlist.items, newItem]
|
||||||
setWishlistItems(newItems)
|
setWishlist({ items: newItems })
|
||||||
await saveWishlistItems(newItems)
|
await saveWishlistItems(newItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
const editWishlistItem = async (updatedItem: WishlistItemType) => {
|
const editWishlistItem = async (updatedItem: WishlistItemType) => {
|
||||||
const newItems = wishlistItems.map(item =>
|
const newItems = wishlist.items.map(item =>
|
||||||
item.id === updatedItem.id ? updatedItem : item
|
item.id === updatedItem.id ? updatedItem : item
|
||||||
)
|
)
|
||||||
setWishlistItems(newItems)
|
setWishlist({ items: newItems })
|
||||||
await saveWishlistItems(newItems)
|
await saveWishlistItems(newItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteWishlistItem = async (id: string) => {
|
const deleteWishlistItem = async (id: string) => {
|
||||||
const newItems = wishlistItems.filter(item => item.id !== id)
|
const newItems = wishlist.items.filter(item => item.id !== id)
|
||||||
setWishlistItems(newItems)
|
setWishlist({ items: newItems })
|
||||||
await saveWishlistItems(newItems)
|
await saveWishlistItems(newItems)
|
||||||
}
|
}
|
||||||
|
|
||||||
const redeemWishlistItem = async (item: WishlistItemType) => {
|
const redeemWishlistItem = async (item: WishlistItemType) => {
|
||||||
if (balance >= item.coinCost) {
|
if (balance >= item.coinCost) {
|
||||||
await removeAmount(
|
const data = await removeCoins(
|
||||||
item.coinCost,
|
item.coinCost,
|
||||||
`Redeemed reward: ${item.name}`,
|
`Redeemed reward: ${item.name}`,
|
||||||
'WISH_REDEMPTION',
|
'WISH_REDEMPTION',
|
||||||
item.id
|
item.id
|
||||||
)
|
)
|
||||||
|
setCoins(data)
|
||||||
|
|
||||||
// Randomly choose a celebration effect
|
// Randomly choose a celebration effect
|
||||||
const celebrationEffects = [
|
const celebrationEffects = [
|
||||||
@@ -71,12 +64,14 @@ export function useWishlist() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canRedeem = (cost: number) => balance >= cost
|
||||||
|
|
||||||
return {
|
return {
|
||||||
wishlistItems,
|
|
||||||
addWishlistItem,
|
addWishlistItem,
|
||||||
editWishlistItem,
|
editWishlistItem,
|
||||||
deleteWishlistItem,
|
deleteWishlistItem,
|
||||||
redeemWishlistItem,
|
redeemWishlistItem,
|
||||||
canRedeem: (cost: number) => balance >= cost
|
canRedeem,
|
||||||
|
wishlistItems: wishlist.items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
lib/atoms.ts
12
lib/atoms.ts
@@ -1,4 +1,12 @@
|
|||||||
import { atom } from "jotai";
|
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());
|
||||||
|
|||||||
@@ -88,3 +88,10 @@ export interface Settings {
|
|||||||
ui: UISettings;
|
ui: UISettings;
|
||||||
system: SystemSettings;
|
system: SystemSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface JotaiHydrateInitialValues {
|
||||||
|
settings: Settings;
|
||||||
|
coins: CoinsData;
|
||||||
|
habits: HabitsData;
|
||||||
|
wishlist: WishlistData;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "habittrove",
|
"name": "habittrove",
|
||||||
"version": "0.1.7",
|
"version": "0.1.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
|
|||||||
Reference in New Issue
Block a user