mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
Added jotai (#14)
* Added jotai * cache settings by using jotai state * use hydrateAtom with SSR * remove useSettings * fix test
This commit is contained in:
@@ -6,10 +6,13 @@
|
|||||||
|
|
||||||
- docker-compose.yaml
|
- docker-compose.yaml
|
||||||
- timezone settings
|
- timezone settings
|
||||||
|
- use jotai for state management
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- completing habits now respect timezone settings
|
- completing habits now respect timezone settings
|
||||||
|
- coin and settings display now respect timezone settings
|
||||||
|
- performance improvements by caching settings
|
||||||
|
|
||||||
## Version 0.1.4
|
## Version 0.1.4
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
WishlistData,
|
WishlistData,
|
||||||
Settings,
|
Settings,
|
||||||
DataType,
|
DataType,
|
||||||
DATA_DEFAULTS
|
DATA_DEFAULTS,
|
||||||
|
getDefaultSettings
|
||||||
} from '@/lib/types'
|
} from '@/lib/types'
|
||||||
import { d2t, getNow, getNowInMilliseconds } from '@/lib/utils';
|
import { d2t, getNow, getNowInMilliseconds } from '@/lib/utils';
|
||||||
|
|
||||||
@@ -121,15 +122,7 @@ export async function addCoins(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function loadSettings(): Promise<Settings> {
|
export async function loadSettings(): Promise<Settings> {
|
||||||
const defaultSettings: Settings = {
|
const defaultSettings = getDefaultSettings()
|
||||||
ui: {
|
|
||||||
useNumberFormatting: true,
|
|
||||||
useGrouping: true,
|
|
||||||
},
|
|
||||||
system: {
|
|
||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await loadData<Settings>('settings')
|
const data = await loadData<Settings>('settings')
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import './globals.css'
|
|||||||
import { Inter } from 'next/font/google'
|
import { Inter } from 'next/font/google'
|
||||||
import { DM_Sans } from 'next/font/google'
|
import { DM_Sans } from 'next/font/google'
|
||||||
import { Toaster } from '@/components/ui/toaster'
|
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'
|
||||||
// Inter (clean, modern, excellent readability)
|
// Inter (clean, modern, excellent readability)
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
@@ -21,15 +25,22 @@ export const metadata = {
|
|||||||
description: 'Track your habits and get rewarded',
|
description: 'Track your habits and get rewarded',
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RootLayout({
|
export default async function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const initialSettings = await loadSettings()
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className={activeFont.className}>
|
<body className={activeFont.className}>
|
||||||
{children}
|
<JotaiProvider>
|
||||||
|
<Suspense fallback="loading">
|
||||||
|
<JotaiHydrate initialSettings={initialSettings}>
|
||||||
|
{children}
|
||||||
|
</JotaiHydrate>
|
||||||
|
</Suspense>
|
||||||
|
</JotaiProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -3,11 +3,20 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Switch } from '@/components/ui/switch'
|
import { Switch } from '@/components/ui/switch'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
|
||||||
import { DynamicTimeNoSSR } from '@/components/DynamicTimeNoSSR'
|
import { DynamicTimeNoSSR } from '@/components/DynamicTimeNoSSR'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
|
import { Settings } from '@/lib/types'
|
||||||
|
import { saveSettings } from '../actions/data'
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { settings, updateSettings } = useSettings()
|
const [settings, setSettings] = useAtom(settingsAtom)
|
||||||
|
|
||||||
|
const updateSettings = async (newSettings: Settings) => {
|
||||||
|
await saveSettings(newSettings)
|
||||||
|
setSettings(newSettings)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!settings) return null
|
if (!settings) return null
|
||||||
|
|
||||||
@@ -89,7 +98,7 @@ export default function SettingsPage() {
|
|||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<DynamicTimeNoSSR timezone={settings.system.timezone} />
|
<DynamicTimeNoSSR />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export default function ChangelogModal({ isOpen, onClose }: ChangelogModalProps)
|
|||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
const loadChangelog = async () => {
|
const loadChangelog = async () => {
|
||||||
const content = await getChangelog()
|
const content = await getChangelog()
|
||||||
console.log(content)
|
|
||||||
setChangelog(content)
|
setChangelog(content)
|
||||||
}
|
}
|
||||||
loadChangelog()
|
loadChangelog()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Coins } from 'lucide-react'
|
import { Coins } from 'lucide-react'
|
||||||
import { formatNumber } from '@/lib/utils/formatNumber'
|
import { formatNumber } from '@/lib/utils/formatNumber'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
|
|
||||||
export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
|
export default function CoinBalance({ coinBalance }: { coinBalance: number }) {
|
||||||
const { settings } = useSettings()
|
const [settings] = useAtom(settingsAtom)
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
|
||||||
import { t2d, d2s, getNow, isSameDate } from '@/lib/utils'
|
import { t2d, d2s, getNow, isSameDate } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { formatNumber } from '@/lib/utils/formatNumber'
|
import { formatNumber } from '@/lib/utils/formatNumber'
|
||||||
@@ -12,10 +11,12 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
|||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { useCoins } from '@/hooks/useCoins'
|
import { useCoins } from '@/hooks/useCoins'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
|
|
||||||
export default function CoinsManager() {
|
export default function CoinsManager() {
|
||||||
const { balance, transactions, addAmount, removeAmount } = useCoins()
|
const { balance, transactions, addAmount, removeAmount } = useCoins()
|
||||||
const { settings } = useSettings()
|
const [settings] = useAtom(settingsAtom)
|
||||||
const DEFAULT_AMOUNT = '0'
|
const DEFAULT_AMOUNT = '0'
|
||||||
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
const [amount, setAmount] = useState(DEFAULT_AMOUNT)
|
||||||
|
|
||||||
@@ -212,7 +213,7 @@ export default function CoinsManager() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{d2s({ dateTime: t2d({ timestamp: transaction.timestamp, timezone: settings.system.timezone }) })}
|
{d2s({ dateTime: t2d({ timestamp: transaction.timestamp, timezone: settings.system.timezone }), timezone: settings.system.timezone })}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp } from 'lucide-react'
|
import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
import { getTodayInTimezone } from '@/lib/utils'
|
import { getTodayInTimezone } from '@/lib/utils'
|
||||||
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'
|
||||||
@@ -24,7 +25,7 @@ export default function DailyOverview({
|
|||||||
onComplete,
|
onComplete,
|
||||||
onUndo
|
onUndo
|
||||||
}: UpcomingItemsProps) {
|
}: UpcomingItemsProps) {
|
||||||
const { settings } = useSettings()
|
const [settings] = useAtom(settingsAtom)
|
||||||
const today = getTodayInTimezone(settings.system.timezone)
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
const todayCompletions = habits.filter(habit =>
|
const todayCompletions = habits.filter(habit =>
|
||||||
habit.completions.includes(today)
|
habit.completions.includes(today)
|
||||||
|
|||||||
@@ -2,14 +2,17 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
import { d2s } from '@/lib/utils'
|
import { d2s, getNow } from '@/lib/utils'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
|
|
||||||
interface DynamicTimeProps {
|
interface DynamicTimeProps {
|
||||||
timezone: string
|
timezone: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DynamicTime({ timezone }: DynamicTimeProps) {
|
export function DynamicTime() {
|
||||||
const [time, setTime] = useState(DateTime.now().setZone(timezone))
|
const [settings] = useAtom(settingsAtom)
|
||||||
|
const [time, setTime] = useState<DateTime>(getNow({ timezone: settings.system.timezone }))
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
@@ -21,7 +24,7 @@ export function DynamicTime({ timezone }: DynamicTimeProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="text-sm text-muted-foreground">
|
<div className="text-sm text-muted-foreground">
|
||||||
{d2s({ dateTime: time })}
|
{d2s({ dateTime: time, timezone: settings.system.timezone })}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ import { Badge } from '@/components/ui/badge'
|
|||||||
import { loadHabitsData } from '@/app/actions/data'
|
import { loadHabitsData } from '@/app/actions/data'
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
import { d2s, getNow } from '@/lib/utils'
|
import { d2s, getNow } from '@/lib/utils'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
import { DateTime } from 'luxon'
|
import { DateTime } from 'luxon'
|
||||||
|
|
||||||
export default function HabitCalendar() {
|
export default function HabitCalendar() {
|
||||||
const { settings } = useSettings()
|
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 [habits, setHabits] = useState<Habit[]>([])
|
||||||
|
|
||||||
@@ -20,12 +21,6 @@ export default function HabitCalendar() {
|
|||||||
fetchHabitsData()
|
fetchHabitsData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Update selectedDate when timezone changes
|
|
||||||
useEffect(() => {
|
|
||||||
const now = getNow({ timezone: settings.system.timezone })
|
|
||||||
setSelectedDate(now)
|
|
||||||
}, [settings])
|
|
||||||
|
|
||||||
const fetchHabitsData = async () => {
|
const fetchHabitsData = async () => {
|
||||||
const data = await loadHabitsData()
|
const data = await loadHabitsData()
|
||||||
setHabits(data.habits)
|
setHabits(data.habits)
|
||||||
@@ -50,7 +45,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,
|
||||||
@@ -65,7 +60,7 @@ export default function HabitCalendar() {
|
|||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
{selectedDate ? (
|
{selectedDate ? (
|
||||||
<>Habits for {d2s({ dateTime: selectedDate })}</>
|
<>Habits for {d2s({ dateTime: selectedDate, timezone: settings.system.timezone })}</>
|
||||||
) : (
|
) : (
|
||||||
'Select a date'
|
'Select a date'
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,8 @@
|
|||||||
import HeatMap from '@uiw/react-heat-map'
|
import HeatMap from '@uiw/react-heat-map'
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
import { getNow } from '@/lib/utils'
|
import { getNow } from '@/lib/utils'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
|
|
||||||
interface HabitHeatmapProps {
|
interface HabitHeatmapProps {
|
||||||
habits: Habit[]
|
habits: Habit[]
|
||||||
@@ -26,7 +27,7 @@ export default function HabitHeatmap({ habits }: HabitHeatmapProps) {
|
|||||||
count
|
count
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const { settings } = useSettings()
|
const [settings] = useAtom(settingsAtom)
|
||||||
|
|
||||||
// Get start date (30 days ago)
|
// Get start date (30 days ago)
|
||||||
const now = getNow({ timezone: settings.system.timezone })
|
const now = getNow({ timezone: settings.system.timezone })
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
import { getTodayInTimezone } from '@/lib/utils'
|
import { getTodayInTimezone } from '@/lib/utils'
|
||||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -15,7 +16,7 @@ interface HabitItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function HabitItem({ habit, onEdit, onDelete, onComplete, onUndo }: HabitItemProps) {
|
export default function HabitItem({ habit, onEdit, onDelete, onComplete, onUndo }: HabitItemProps) {
|
||||||
const { settings } = useSettings()
|
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)
|
||||||
const [isHighlighted, setIsHighlighted] = useState(false)
|
const [isHighlighted, setIsHighlighted] = useState(false)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
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 { useEffect, useState } from 'react'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
|
||||||
import { getTodayInTimezone } from '@/lib/utils'
|
import { getTodayInTimezone } from '@/lib/utils'
|
||||||
import { loadHabitsData } from '@/app/actions/data'
|
import { loadHabitsData } from '@/app/actions/data'
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
|
|
||||||
export default function HabitOverview() {
|
export default function HabitOverview() {
|
||||||
const [habits, setHabits] = useState<Habit[]>([])
|
const [habits, setHabits] = useState<Habit[]>([])
|
||||||
@@ -17,7 +18,7 @@ export default function HabitOverview() {
|
|||||||
fetchHabits()
|
fetchHabits()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const { settings } = useSettings()
|
const [settings] = useAtom(settingsAtom)
|
||||||
const today = getTodayInTimezone(settings.system.timezone)
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
|
|
||||||
const completedToday = habits.filter(habit =>
|
const completedToday = habits.filter(habit =>
|
||||||
|
|||||||
@@ -2,20 +2,21 @@
|
|||||||
|
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
|
||||||
import { d2s, getNow } from '@/lib/utils'
|
import { d2s, getNow } from '@/lib/utils'
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||||
|
import { useAtom } from 'jotai'
|
||||||
|
import { settingsAtom } from '@/lib/atoms'
|
||||||
|
|
||||||
interface HabitStreakProps {
|
interface HabitStreakProps {
|
||||||
habits: Habit[]
|
habits: Habit[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function HabitStreak({ habits }: HabitStreakProps) {
|
export default function HabitStreak({ habits }: HabitStreakProps) {
|
||||||
const { settings } = useSettings()
|
const [settings] = useAtom(settingsAtom)
|
||||||
// Get the last 7 days of data
|
// Get the last 7 days of data
|
||||||
const dates = Array.from({ length: 7 }, (_, i) => {
|
const dates = Array.from({ length: 7 }, (_, i) => {
|
||||||
const d = getNow({ timezone: settings.system.timezone });
|
const d = getNow({ timezone: settings.system.timezone });
|
||||||
return d2s({ dateTime: d.minus({ days: i }), format: 'yyyy-MM-dd' });
|
return d2s({ dateTime: d.minus({ days: i }), format: 'yyyy-MM-dd', timezone: settings.system.timezone });
|
||||||
}).reverse()
|
}).reverse()
|
||||||
|
|
||||||
const completions = dates.map(date => {
|
const completions = dates.map(date => {
|
||||||
|
|||||||
16
components/jotai-hydrate.tsx
Normal file
16
components/jotai-hydrate.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { settingsAtom } from "@/lib/atoms"
|
||||||
|
import { useHydrateAtoms } from "jotai/utils"
|
||||||
|
import { Settings } from "@/lib/types"
|
||||||
|
|
||||||
|
export function JotaiHydrate({
|
||||||
|
children,
|
||||||
|
initialSettings
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
initialSettings: Settings
|
||||||
|
}) {
|
||||||
|
useHydrateAtoms([[settingsAtom, initialSettings]])
|
||||||
|
return children
|
||||||
|
}
|
||||||
11
components/jotai-providers.tsx
Normal file
11
components/jotai-providers.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Provider } from 'jotai'
|
||||||
|
|
||||||
|
export const JotaiProvider = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
return (
|
||||||
|
<Provider>
|
||||||
|
{children}
|
||||||
|
</Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
|
||||||
import { loadHabitsData, saveHabitsData, addCoins, removeCoins } from '@/app/actions/data'
|
import { loadHabitsData, saveHabitsData, addCoins, removeCoins } from '@/app/actions/data'
|
||||||
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 { Habit } from '@/lib/types'
|
||||||
import { getNowInMilliseconds, getTodayInTimezone } from '@/lib/utils'
|
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 [habits, setHabits] = useState<Habit[]>([])
|
||||||
const { settings } = useSettings()
|
const [settings] = useAtom(settingsAtom)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchHabits()
|
fetchHabits()
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
'use client'
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { getDefaultSettings, Settings } from '@/lib/types'
|
|
||||||
import { loadSettings, saveSettings } from '@/app/actions/data'
|
|
||||||
|
|
||||||
export function useSettings() {
|
|
||||||
const [settings, setSettings] = useState<Settings>(getDefaultSettings()) // TODO: do we need to initialize the settings here?
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadSettings().then(setSettings)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const updateSettings = async (newSettings: Settings) => {
|
|
||||||
await saveSettings(newSettings)
|
|
||||||
setSettings(newSettings)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
settings,
|
|
||||||
updateSettings,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
4
lib/atoms.ts
Normal file
4
lib/atoms.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { atom } from "jotai";
|
||||||
|
import { getDefaultSettings } from "./types";
|
||||||
|
|
||||||
|
export const settingsAtom = atom(getDefaultSettings())
|
||||||
@@ -9,7 +9,7 @@ export function cn(...inputs: ClassValue[]) {
|
|||||||
// get today's date string for timezone
|
// get today's date string for timezone
|
||||||
export function getTodayInTimezone(timezone: string): string {
|
export function getTodayInTimezone(timezone: string): string {
|
||||||
const now = getNow({ timezone });
|
const now = getNow({ timezone });
|
||||||
return d2s({ dateTime: now, format: 'yyyy-MM-dd' });
|
return d2s({ dateTime: now, format: 'yyyy-MM-dd', timezone });
|
||||||
}
|
}
|
||||||
|
|
||||||
// get datetime object of now
|
// get datetime object of now
|
||||||
@@ -34,11 +34,11 @@ export function d2t({ dateTime, timezone = 'utc' }: { dateTime: DateTime, timezo
|
|||||||
}
|
}
|
||||||
|
|
||||||
// convert datetime object to string, mostly for display
|
// convert datetime object to string, mostly for display
|
||||||
export function d2s({ dateTime, format }: { dateTime: DateTime, format?: string }) {
|
export function d2s({ dateTime, format, timezone }: { dateTime: DateTime, format?: string, timezone: string }) {
|
||||||
if (format) {
|
if (format) {
|
||||||
return dateTime.toFormat(format);
|
return dateTime.setZone(timezone).toFormat(format);
|
||||||
}
|
}
|
||||||
return dateTime.toLocaleString(DateTime.DATETIME_MED);
|
return dateTime.setZone(timezone).toLocaleString(DateTime.DATETIME_MED);
|
||||||
}
|
}
|
||||||
|
|
||||||
// convert datetime object to date string, mostly for display
|
// convert datetime object to date string, mostly for display
|
||||||
|
|||||||
21
package-lock.json
generated
21
package-lock.json
generated
@@ -23,6 +23,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"jotai": "^2.8.0",
|
||||||
"js-confetti": "^0.12.0",
|
"js-confetti": "^0.12.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
@@ -5068,6 +5069,26 @@
|
|||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jotai": {
|
||||||
|
"version": "2.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jotai/-/jotai-2.8.0.tgz",
|
||||||
|
"integrity": "sha512-yZNMC36FdLOksOr8qga0yLf14miCJlEThlp5DeFJNnqzm2+ZG7wLcJzoOyij5K6U6Xlc5ljQqPDlJRgqW0Y18g==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=17.0.0",
|
||||||
|
"react": ">=17.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/js-confetti": {
|
"node_modules/js-confetti": {
|
||||||
"version": "0.12.0",
|
"version": "0.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-confetti/-/js-confetti-0.12.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-confetti/-/js-confetti-0.12.0.tgz",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"jotai": "^2.8.0",
|
||||||
"js-confetti": "^0.12.0",
|
"js-confetti": "^0.12.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"luxon": "^3.5.0",
|
"luxon": "^3.5.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user