Skip to content

Commit

Permalink
use jotai for all states (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
dohsimpson authored Jan 4, 2025
1 parent 306242f commit ad05a46
Show file tree
Hide file tree
Showing 18 changed files with 212 additions and 243 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Version 0.1.8

### Changed

- use jotai for all state management

## Version 0.1.7

### Fixed
Expand Down
8 changes: 6 additions & 2 deletions app/actions/data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(type: DataType): T {
return DATA_DEFAULTS[type]() as T;
Expand Down Expand Up @@ -65,8 +65,12 @@ async function saveData<T>(type: DataType, data: T): Promise<void> {
}

// Wishlist specific functions
export async function loadWishlistData(): Promise<WishlistData> {
return loadData<WishlistData>('wishlist')
}

export async function loadWishlistItems(): Promise<WishlistItemType[]> {
const data = await loadData<WishlistData>('wishlist')
const data = await loadWishlistData()
return data.items
}

Expand Down
19 changes: 16 additions & 3 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'],
Expand All @@ -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 (
<html lang="en">
<body className={activeFont.className}>
<JotaiProvider>
<Suspense fallback="loading">
<JotaiHydrate initialSettings={initialSettings}>
<JotaiHydrate
initialValues={{
settings: initialSettings,
habits: initialHabits,
coins: initialCoins,
wishlist: initialWishlist
}}
>
{children}
</JotaiHydrate>
</Suspense>
Expand Down
33 changes: 10 additions & 23 deletions components/CoinsManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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` })
}
}

Expand Down Expand Up @@ -84,14 +78,7 @@ export default function CoinsManager() {
</div>

<Button
onClick={() => {
const numAmount = Number(amount);
if (numAmount > 0) {
handleAddCoins();
} else if (numAmount < 0) {
handleRemoveCoins();
}
}}
onClick={handleAddRemoveCoins}
className="w-full h-14 transition-colors flex items-center justify-center font-medium"
variant="default"
>
Expand Down
41 changes: 13 additions & 28 deletions components/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,21 @@
'use client'

import { loadCoinsData } from '@/app/actions/data'
import { useHabits } from '@/hooks/useHabits'
import { useWishlist } from '@/hooks/useWishlist'
import { useEffect, useState } from 'react'
import { useAtom } from 'jotai'
import { wishlistAtom, habitsAtom, settingsAtom, coinsAtom } from '@/lib/atoms'
import CoinBalance from './CoinBalance'
import DailyOverview from './DailyOverview'
import HabitOverview from './HabitOverview'
import HabitStreak from './HabitStreak'
import { useHabits } from '@/hooks/useHabits'

export default function Dashboard() {
const { habits, completeHabit, undoComplete } = useHabits()
const [coinBalance, setCoinBalance] = useState(0)
const { wishlistItems } = useWishlist()

useEffect(() => {
const loadData = async () => {
const coinsData = await loadCoinsData()
setCoinBalance(coinsData.balance)
}
loadData()
}, [])
const { completeHabit, undoComplete } = useHabits()
const [habitsData] = useAtom(habitsAtom)
const habits = habitsData.habits
const [settings] = useAtom(settingsAtom)
const [coins] = useAtom(coinsAtom)
const coinBalance = coins.balance
const [wishlist] = useAtom(wishlistAtom)
const wishlistItems = wishlist.items

return (
<div className="container mx-auto px-4 py-8">
Expand All @@ -33,18 +28,8 @@ export default function Dashboard() {
wishlistItems={wishlistItems}
habits={habits}
coinBalance={coinBalance}
onComplete={async (habit) => {
const newBalance = await completeHabit(habit)
if (newBalance !== null) {
setCoinBalance(newBalance)
}
}}
onUndo={async (habit) => {
const newBalance = await undoComplete(habit)
if (newBalance !== null) {
setCoinBalance(newBalance)
}
}}
onComplete={completeHabit}
onUndo={undoComplete}
/>

{/* <HabitHeatmap habits={habits} /> */}
Expand Down
21 changes: 5 additions & 16 deletions components/HabitCalendar.tsx
Original file line number Diff line number Diff line change
@@ -1,31 +1,20 @@
'use client'

import { useEffect, useState } from 'react'
import { useState } from 'react'
import { Calendar } from '@/components/ui/calendar'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'

import { loadHabitsData } from '@/app/actions/data'
import { Habit } from '@/lib/types'
import { d2s, getNow } from '@/lib/utils'
import { useAtom } from 'jotai'
import { settingsAtom } from '@/lib/atoms'
import { habitsAtom, settingsAtom } from '@/lib/atoms'
import { DateTime } from 'luxon'
import Linkify from './linkify'

export default function HabitCalendar() {
const [settings] = useAtom(settingsAtom)
const [selectedDate, setSelectedDate] = useState<DateTime>(getNow({ timezone: settings.system.timezone }))
const [habits, setHabits] = useState<Habit[]>([])

useEffect(() => {
fetchHabitsData()
}, [])

const fetchHabitsData = async () => {
const data = await loadHabitsData()
setHabits(data.habits)
}
const [habitsData] = useAtom(habitsAtom)
const habits = habitsData.habits

const getHabitsForDate = (date: Date) => {
const dateString = date.toISOString().split('T')[0]
Expand All @@ -46,7 +35,7 @@ export default function HabitCalendar() {
<Calendar
mode="single"
selected={selectedDate.toJSDate()}
// onSelect={(e) => e && setSelectedDate(DateTime.fromJSDate(e))}
onSelect={(e) => e && setSelectedDate(DateTime.fromJSDate(e))}
className="rounded-md border"
modifiers={{
completed: (date) => getHabitsForDate(date).length > 0,
Expand Down
12 changes: 6 additions & 6 deletions components/HabitItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { Button } from '@/components/ui/button'
import { Coins, Edit, Trash2, Check, Undo2 } from 'lucide-react'
import { useEffect, useState } from 'react'
import { useHabits } from '@/hooks/useHabits'

interface HabitItemProps {
habit: Habit
onEdit: () => 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 today = getTodayInTimezone(settings.system.timezone)
const isCompletedToday = habit.completions?.includes(today)
Expand All @@ -41,7 +41,7 @@ export default function HabitItem({ habit, onEdit, onDelete, onComplete, onUndo
}, [habit.id])

return (
<Card
<Card
id={`habit-${habit.id}`}
className={`transition-all duration-500 ${isHighlighted ? 'bg-yellow-100 dark:bg-yellow-900' : ''}`}
>
Expand Down Expand Up @@ -71,7 +71,7 @@ export default function HabitItem({ habit, onEdit, onDelete, onComplete, onUndo
<Button
variant={isCompletedToday ? "secondary" : "default"}
size="sm"
onClick={onComplete}
onClick={async () => await completeHabit(habit)}
disabled={isCompletedToday}
>
<Check className="h-4 w-4 mr-2" />
Expand All @@ -81,7 +81,7 @@ export default function HabitItem({ habit, onEdit, onDelete, onComplete, onUndo
<Button
variant="outline"
size="sm"
onClick={onUndo}
onClick={async () => await undoComplete(habit)}
>
<Undo2 />
</Button>
Expand Down
39 changes: 19 additions & 20 deletions components/HabitList.tsx
Original file line number Diff line number Diff line change
@@ -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<Habit | null>(null)
const [deleteConfirmation, setDeleteConfirmation] = useState<{ isOpen: boolean, habitId: string | null }>({
Expand Down Expand Up @@ -39,17 +44,15 @@ export default function HabitList() {
</div>
) : (
habits.map((habit) => (
<HabitItem
key={habit.id}
habit={habit}
onEdit={() => {
setEditingHabit(habit)
setIsModalOpen(true)
}}
onDelete={() => setDeleteConfirmation({ isOpen: true, habitId: habit.id })}
onComplete={() => completeHabit(habit)}
onUndo={() => undoComplete(habit)}
/>
<HabitItem
key={habit.id}
habit={habit}
onEdit={() => {
setEditingHabit(habit)
setIsModalOpen(true)
}}
onDelete={() => setDeleteConfirmation({ isOpen: true, habitId: habit.id })}
/>
))
)}
</div>
Expand All @@ -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)
}}
Expand All @@ -73,9 +72,9 @@ export default function HabitList() {
<ConfirmDialog
isOpen={deleteConfirmation.isOpen}
onClose={() => setDeleteConfirmation({ isOpen: false, habitId: null })}
onConfirm={() => {
onConfirm={async () => {
if (deleteConfirmation.habitId) {
deleteHabit(deleteConfirmation.habitId)
await deleteHabit(deleteConfirmation.habitId)
}
setDeleteConfirmation({ isOpen: false, habitId: null })
}}
Expand Down
16 changes: 3 additions & 13 deletions components/HabitOverview.tsx
Original file line number Diff line number Diff line change
@@ -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<Habit[]>([])

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)
Expand Down
Loading

0 comments on commit ad05a46

Please sign in to comment.