fix: migrate atoms to normal functions

This commit is contained in:
2026-03-06 23:16:19 +01:00
parent f7034116a3
commit 630363af1f
5 changed files with 47 additions and 91 deletions

View File

@@ -10,7 +10,7 @@ import { useCoins } from '@/hooks/useCoins'
import { currentUserAtom, settingsAtom, usersAtom } from '@/lib/atoms' import { currentUserAtom, settingsAtom, usersAtom } from '@/lib/atoms'
import { MAX_COIN_LIMIT } from '@/lib/constants' import { MAX_COIN_LIMIT } from '@/lib/constants'
import { TransactionType } from '@/lib/types' import { TransactionType } from '@/lib/types'
import { d2s, t2d } from '@/lib/utils' import { calculateTransactionsToday, d2s, t2d } from '@/lib/utils'
import { useAtom } from 'jotai' import { useAtom } from 'jotai'
import { History } from 'lucide-react' import { History } from 'lucide-react'
import { useTranslations } from 'next-intl' import { useTranslations } from 'next-intl'
@@ -33,8 +33,7 @@ export default function CoinsManager() {
coinsEarnedToday, coinsEarnedToday,
totalEarned, totalEarned,
totalSpent, totalSpent,
coinsSpentToday, coinsSpentToday
transactionsToday
} = useCoins({ selectedUser }) } = useCoins({ selectedUser })
const [settings] = useAtom(settingsAtom) const [settings] = useAtom(settingsAtom)
const [usersData] = useAtom(usersAtom) const [usersData] = useAtom(usersAtom)
@@ -252,7 +251,7 @@ export default function CoinsManager() {
<div className="p-4 rounded-lg bg-orange-100 dark:bg-orange-900"> <div className="p-4 rounded-lg bg-orange-100 dark:bg-orange-900">
<div className="text-sm text-orange-800 dark:text-orange-100 mb-1">{t('todaysTransactionsLabel')}</div> <div className="text-sm text-orange-800 dark:text-orange-100 mb-1">{t('todaysTransactionsLabel')}</div>
<div className="text-2xl font-bold text-orange-900 dark:text-orange-50"> <div className="text-2xl font-bold text-orange-900 dark:text-orange-50">
{transactionsToday} 📊 {calculateTransactionsToday(transactions, settings.system.timezone)} 📊
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,15 +2,14 @@ import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data';
import { toast } from '@/hooks/use-toast'; import { toast } from '@/hooks/use-toast';
import { import {
coinsAtom, coinsAtom,
coinsBalanceAtom,
coinsEarnedTodayAtom, coinsEarnedTodayAtom,
coinsSpentTodayAtom, coinsSpentTodayAtom,
currentUserAtom, currentUserAtom,
currentUserIdAtom,
settingsAtom, settingsAtom,
totalEarnedAtom, totalEarnedAtom,
totalSpentAtom, totalSpentAtom,
transactionsTodayAtom, usersAtom
usersAtom,
} from '@/lib/atoms'; } from '@/lib/atoms';
import { MAX_COIN_LIMIT } from '@/lib/constants'; import { MAX_COIN_LIMIT } from '@/lib/constants';
import { CoinsData } from '@/lib/types'; import { CoinsData } from '@/lib/types';
@@ -24,27 +23,26 @@ export function useCoins(options?: { selectedUser?: string }) {
const tCommon = useTranslations('Common'); const tCommon = useTranslations('Common');
const [coins, setCoins] = useAtom(coinsAtom) const [coins, setCoins] = useAtom(coinsAtom)
const [settings] = useAtom(settingsAtom) const [settings] = useAtom(settingsAtom)
const [users] = useAtom(usersAtom) const [{users}] = useAtom(usersAtom)
const [currentUser] = useAtom(currentUserAtom) const [currentUser] = useAtom(currentUserAtom)
const [allCoinsData] = useAtom(coinsAtom) // All coin transactions const [coinsData] = useAtom(coinsAtom) // All coin transactions
const [loggedInUserBalance] = useAtom(coinsBalanceAtom) // Balance of the *currently logged-in* user const [loggedInUserId] = useAtom(currentUserIdAtom);
const loggedInUserBalance = loggedInUserId ? coins.transactions.filter(transaction => transaction.userId === loggedInUserId).reduce((sum, transaction) => sum + transaction.amount, 0) : 0;
const [atomCoinsEarnedToday] = useAtom(coinsEarnedTodayAtom); const [atomCoinsEarnedToday] = useAtom(coinsEarnedTodayAtom);
const [atomTotalEarned] = useAtom(totalEarnedAtom) const [atomTotalEarned] = useAtom(totalEarnedAtom)
const [atomTotalSpent] = useAtom(totalSpentAtom) const [atomTotalSpent] = useAtom(totalSpentAtom)
const [atomCoinsSpentToday] = useAtom(coinsSpentTodayAtom); const [atomCoinsSpentToday] = useAtom(coinsSpentTodayAtom);
const [atomTransactionsToday] = useAtom(transactionsTodayAtom); const targetUser = options?.selectedUser ? users.find(u => u.id === options.selectedUser) : currentUser
const targetUser = options?.selectedUser ? users.users.find(u => u.id === options.selectedUser) : currentUser
const transactions = useMemo(() => { const transactions = useMemo(() => {
return allCoinsData.transactions.filter(t => t.userId === targetUser?.id); return coinsData.transactions.filter(t => t.userId === targetUser?.id);
}, [allCoinsData, targetUser?.id]); }, [coinsData, targetUser?.id]);
const timezone = settings.system.timezone; const timezone = settings.system.timezone;
const [coinsEarnedToday, setCoinsEarnedToday] = useState(0); const [coinsEarnedToday, setCoinsEarnedToday] = useState(0);
const [totalEarned, setTotalEarned] = useState(0); const [totalEarned, setTotalEarned] = useState(0);
const [totalSpent, setTotalSpent] = useState(0); const [totalSpent, setTotalSpent] = useState(0);
const [coinsSpentToday, setCoinsSpentToday] = useState(0); const [coinsSpentToday, setCoinsSpentToday] = useState(0);
const [transactionsToday, setTransactionsToday] = useState<number>(0);
const [balance, setBalance] = useState(0); const [balance, setBalance] = useState(0);
useEffect(() => { useEffect(() => {
@@ -55,7 +53,6 @@ export function useCoins(options?: { selectedUser?: string }) {
setTotalEarned(atomTotalEarned); setTotalEarned(atomTotalEarned);
setTotalSpent(atomTotalSpent); setTotalSpent(atomTotalSpent);
setCoinsSpentToday(atomCoinsSpentToday); setCoinsSpentToday(atomCoinsSpentToday);
setTransactionsToday(atomTransactionsToday);
setBalance(loggedInUserBalance); setBalance(loggedInUserBalance);
} else if (targetUser?.id) { } else if (targetUser?.id) {
// If an admin is viewing another user, calculate their metrics manually // If an admin is viewing another user, calculate their metrics manually
@@ -71,8 +68,6 @@ export function useCoins(options?: { selectedUser?: string }) {
const spentToday = calculateCoinsSpentToday(transactions, timezone); const spentToday = calculateCoinsSpentToday(transactions, timezone);
setCoinsSpentToday(roundToInteger(spentToday)); setCoinsSpentToday(roundToInteger(spentToday));
setTransactionsToday(calculateTransactionsToday(transactions, timezone)); // This is a count
const calculatedBalance = transactions.reduce((acc, t) => acc + t.amount, 0); const calculatedBalance = transactions.reduce((acc, t) => acc + t.amount, 0);
setBalance(roundToInteger(calculatedBalance)); setBalance(roundToInteger(calculatedBalance));
} }
@@ -85,8 +80,7 @@ export function useCoins(options?: { selectedUser?: string }) {
atomCoinsEarnedToday, atomCoinsEarnedToday,
atomTotalEarned, atomTotalEarned,
atomTotalSpent, atomTotalSpent,
atomCoinsSpentToday, atomCoinsSpentToday
atomTransactionsToday,
]); ]);
const add = async (amount: number, description: string, note?: string) => { const add = async (amount: number, description: string, note?: string) => {
@@ -187,7 +181,6 @@ export function useCoins(options?: { selectedUser?: string }) {
coinsEarnedToday, coinsEarnedToday,
totalEarned, totalEarned,
totalSpent, totalSpent,
coinsSpentToday, coinsSpentToday
transactionsToday
} }
} }

View File

@@ -3,10 +3,7 @@ import {
calculateCoinsSpentToday, calculateCoinsSpentToday,
calculateTotalEarned, calculateTotalEarned,
calculateTotalSpent, calculateTotalSpent,
calculateTransactionsToday,
generateCryptoHash, generateCryptoHash,
getCompletionsForToday,
getHabitFreq,
isHabitDue, isHabitDue,
prepareDataForHashing, prepareDataForHashing,
roundToInteger, roundToInteger,
@@ -16,9 +13,9 @@ import { atom } from "jotai";
import { atomFamily, atomWithStorage } from "jotai/utils"; import { atomFamily, atomWithStorage } from "jotai/utils";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { import {
BrowserSettings,
CoinsData, CoinsData,
CompletionCache, CompletionCache,
Freq,
getDefaultCoinsData, getDefaultCoinsData,
getDefaultHabitsData, getDefaultHabitsData,
getDefaultServerSettings, getDefaultServerSettings,
@@ -27,6 +24,7 @@ import {
getDefaultWishlistData, getDefaultWishlistData,
Habit, Habit,
HabitsData, HabitsData,
PomodoroAtom,
ServerSettings, ServerSettings,
Settings, Settings,
UserData, UserData,
@@ -34,12 +32,6 @@ import {
WishlistData WishlistData
} from "./types"; } from "./types";
export interface BrowserSettings {
expandedHabits: boolean
expandedTasks: boolean
expandedWishlist: boolean
}
export const browserSettingsAtom = atomWithStorage('browserSettings', { export const browserSettingsAtom = atomWithStorage('browserSettings', {
expandedHabits: false, expandedHabits: false,
expandedTasks: false, expandedTasks: false,
@@ -47,11 +39,21 @@ export const browserSettingsAtom = atomWithStorage('browserSettings', {
} as BrowserSettings) } as BrowserSettings)
export const usersAtom = atom(getDefaultUsersData<UserData>()) export const usersAtom = atom(getDefaultUsersData<UserData>())
export const currentUserIdAtom = atom<UserId | undefined>(undefined);
export const settingsAtom = atom(getDefaultSettings<Settings>()); export const settingsAtom = atom(getDefaultSettings<Settings>());
export const habitsAtom = atom(getDefaultHabitsData<HabitsData>()); export const habitsAtom = atom(getDefaultHabitsData<HabitsData>());
export const coinsAtom = atom(getDefaultCoinsData<CoinsData>()); export const coinsAtom = atom(getDefaultCoinsData<CoinsData>());
export const wishlistAtom = atom(getDefaultWishlistData<WishlistData>()); export const wishlistAtom = atom(getDefaultWishlistData<WishlistData>());
export const serverSettingsAtom = atom(getDefaultServerSettings<ServerSettings>()); export const serverSettingsAtom = atom(getDefaultServerSettings<ServerSettings>());
export const userSelectAtom = atom<boolean>(false)
export const aboutOpenAtom = atom<boolean>(false)
export const pomodoroAtom = atom<PomodoroAtom>({
show: false,
selectedHabitId: null,
autoStart: true,
minimized: false,
})
// Derived atom for coins earned today // Derived atom for coins earned today
export const coinsEarnedTodayAtom = atom((get) => { export const coinsEarnedTodayAtom = atom((get) => {
@@ -83,54 +85,12 @@ export const coinsSpentTodayAtom = atom((get) => {
return roundToInteger(value); return roundToInteger(value);
}); });
// Derived atom for transactions today
export const transactionsTodayAtom = atom((get) => {
const coins = get(coinsAtom);
const settings = get(settingsAtom);
return calculateTransactionsToday(coins.transactions, settings.system.timezone);
});
// Atom to store the current logged-in user's ID.
// This should be set by your application when the user session is available.
export const currentUserIdAtom = atom<UserId | undefined>(undefined);
export const currentUserAtom = atom((get) => { export const currentUserAtom = atom((get) => {
const currentUserId = get(currentUserIdAtom); const currentUserId = get(currentUserIdAtom);
const users = get(usersAtom); const users = get(usersAtom);
return users.users.find(user => user.id === currentUserId); return users.users.find(user => user.id === currentUserId);
}) })
// Derived atom for current balance for the logged-in user
export const coinsBalanceAtom = atom((get) => {
const loggedInUserId = get(currentUserIdAtom);
if (!loggedInUserId) {
return 0; // No user logged in or ID not set, so balance is 0
}
const coins = get(coinsAtom);
const balance = coins.transactions
.filter(transaction => transaction.userId === loggedInUserId)
.reduce((sum, transaction) => sum + transaction.amount, 0);
return roundToInteger(balance);
});
/* transient atoms */
export interface PomodoroAtom {
show: boolean
selectedHabitId: string | null
autoStart: boolean
minimized: boolean
}
export const pomodoroAtom = atom<PomodoroAtom>({
show: false,
selectedHabitId: null,
autoStart: true,
minimized: false,
})
export const userSelectAtom = atom<boolean>(false)
export const aboutOpenAtom = atom<boolean>(false)
/** /**
* Asynchronous atom that calculates a freshness token (hash) based on the current client-side data. * Asynchronous atom that calculates a freshness token (hash) based on the current client-side data.
* This token can be compared with a server-generated token to detect data discrepancies. * This token can be compared with a server-generated token to detect data discrepancies.
@@ -147,34 +107,26 @@ export const clientFreshnessTokenAtom = atom(async (get) => {
return hash; return hash;
}); });
// Derived atom for completion cache // Derived atom for completed habits by date, using the cache
export const completionCacheAtom = atom((get) => { export const completedHabitsMapAtom = atom((get) => {
const habits = get(habitsAtom).habits; const habits = get(habitsAtom).habits;
const completionCache: CompletionCache = {};
const map = new Map<string, Habit[]>();
const timezone = get(settingsAtom).system.timezone; const timezone = get(settingsAtom).system.timezone;
const cache: CompletionCache = {};
habits.forEach(habit => { habits.forEach(habit => {
habit.completions.forEach(utcTimestamp => { habit.completions.forEach(utcTimestamp => {
const localDate = t2d({ timestamp: utcTimestamp, timezone }) const localDate = t2d({ timestamp: utcTimestamp, timezone })
.toFormat('yyyy-MM-dd'); .toFormat('yyyy-MM-dd');
if (!cache[localDate]) { if (!completionCache[localDate]) {
cache[localDate] = {}; completionCache[localDate] = {};
} }
cache[localDate][habit.id] = (cache[localDate][habit.id] || 0) + 1; completionCache[localDate][habit.id] = (completionCache[localDate][habit.id] || 0) + 1;
}); });
}); });
return cache;
});
// Derived atom for completed habits by date, using the cache
export const completedHabitsMapAtom = atom((get) => {
const habits = get(habitsAtom).habits;
const completionCache = get(completionCacheAtom);
const map = new Map<string, Habit[]>();
// For each date in the cache // For each date in the cache
Object.entries(completionCache).forEach(([dateKey, habitCompletions]) => { Object.entries(completionCache).forEach(([dateKey, habitCompletions]) => {
const completedHabits = habits.filter(habit => { const completedHabits = habits.filter(habit => {

View File

@@ -210,3 +210,16 @@ export interface ParsedFrequencyResult {
message: string | null message: string | null
result: ParsedResultType result: ParsedResultType
} }
export interface PomodoroAtom {
show: boolean
selectedHabitId: string | null
autoStart: boolean
minimized: boolean
}
export interface BrowserSettings {
expandedHabits: boolean
expandedTasks: boolean
expandedWishlist: boolean
}

View File

@@ -1,12 +1,11 @@
import { toast } from "@/hooks/use-toast" import { toast } from "@/hooks/use-toast"
import { CoinsData, CoinTransaction, Freq, Habit, HabitsData, ParsedFrequencyResult, ParsedResultType, SafeUser, Settings, User, UserData, WishlistData } from '@/lib/types' import { CoinsData, CoinTransaction, Freq, Habit, HabitsData, ParsedFrequencyResult, ParsedResultType, PomodoroAtom, SafeUser, Settings, User, UserData, WishlistData } from '@/lib/types'
import * as chrono from 'chrono-node' import * as chrono from 'chrono-node'
import { clsx, type ClassValue } from "clsx" import { clsx, type ClassValue } from "clsx"
import { DateTime, DateTimeFormatOptions } from "luxon" import { DateTime, DateTimeFormatOptions } from "luxon"
import { Formats } from "next-intl" import { Formats } from "next-intl"
import { datetime, RRule } from 'rrule' import { datetime, RRule } from 'rrule'
import { twMerge } from "tailwind-merge" import { twMerge } from "tailwind-merge"
import { PomodoroAtom } from "./atoms"
import { DUE_MAP, INITIAL_DUE, RECURRENCE_RULE_MAP } from "./constants" import { DUE_MAP, INITIAL_DUE, RECURRENCE_RULE_MAP } from "./constants"
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {