Merge Tag v0.2.23

This commit is contained in:
2025-06-13 21:59:16 +02:00
13 changed files with 101 additions and 32 deletions

View File

@@ -8,6 +8,7 @@ import {
getHabitFreq,
getTodayInTimezone,
isHabitDue,
roundToInteger,
t2d
} from "@/lib/utils";
import { atom } from "jotai";
@@ -49,26 +50,30 @@ export const serverSettingsAtom = atom(getDefaultServerSettings());
export const coinsEarnedTodayAtom = atom((get) => {
const coins = get(coinsAtom);
const settings = get(settingsAtom);
return calculateCoinsEarnedToday(coins.transactions, settings.system.timezone);
const value = calculateCoinsEarnedToday(coins.transactions, settings.system.timezone);
return roundToInteger(value);
});
// Derived atom for total earned
export const totalEarnedAtom = atom((get) => {
const coins = get(coinsAtom);
return calculateTotalEarned(coins.transactions);
const value = calculateTotalEarned(coins.transactions);
return roundToInteger(value);
});
// Derived atom for total spent
export const totalSpentAtom = atom((get) => {
const coins = get(coinsAtom);
return calculateTotalSpent(coins.transactions);
const value = calculateTotalSpent(coins.transactions);
return roundToInteger(value);
});
// Derived atom for coins spent today
export const coinsSpentTodayAtom = atom((get) => {
const coins = get(coinsAtom);
const settings = get(settingsAtom);
return calculateCoinsSpentToday(coins.transactions, settings.system.timezone);
const value = calculateCoinsSpentToday(coins.transactions, settings.system.timezone);
return roundToInteger(value);
});
// Derived atom for transactions today
@@ -95,9 +100,10 @@ export const coinsBalanceAtom = atom((get) => {
return 0; // No user logged in or ID not set, so balance is 0
}
const coins = get(coinsAtom);
return coins.transactions
const balance = coins.transactions
.filter(transaction => transaction.userId === loggedInUserId)
.reduce((sum, transaction) => sum + transaction.amount, 0);
return roundToInteger(balance);
});
/* transient atoms */
@@ -115,7 +121,7 @@ export const pomodoroAtom = atom<PomodoroAtom>({
minimized: false,
})
import { prepareDataForHashing, generateCryptoHash } from '@/lib/utils';
import { generateCryptoHash, prepareDataForHashing } from '@/lib/utils';
export const userSelectAtom = atom<boolean>(false)
export const aboutOpenAtom = atom<boolean>(false)

View File

@@ -22,9 +22,10 @@ import {
serializeRRule,
convertHumanReadableFrequencyToMachineReadable,
convertMachineReadableFrequencyToHumanReadable,
getUnsupportedRRuleReason,
prepareDataForHashing,
generateCryptoHash
generateCryptoHash,
getUnsupportedRRuleReason,
roundToInteger
} from './utils'
import { CoinTransaction, ParsedResultType, Settings, HabitsData, CoinsData, WishlistData, UserData } from './types'
import { DateTime } from "luxon";
@@ -42,6 +43,33 @@ describe('cn utility', () => {
})
})
describe('roundToInteger', () => {
test('should round positive numbers correctly', () => {
expect(roundToInteger(10.123)).toBe(10);
expect(roundToInteger(10.5)).toBe(11);
expect(roundToInteger(10.75)).toBe(11);
expect(roundToInteger(10.49)).toBe(10);
});
test('should round negative numbers correctly', () => {
expect(roundToInteger(-10.123)).toBe(-10);
expect(roundToInteger(-10.5)).toBe(-10); // Math.round rounds -x.5 to -(x-1) e.g. -10.5 to -10
expect(roundToInteger(-10.75)).toBe(-11);
expect(roundToInteger(-10.49)).toBe(-10);
});
test('should handle zero correctly', () => {
expect(roundToInteger(0)).toBe(0);
expect(roundToInteger(0.0)).toBe(0);
expect(roundToInteger(-0.0)).toBe(-0);
});
test('should handle integers correctly', () => {
expect(roundToInteger(15)).toBe(15);
expect(roundToInteger(-15)).toBe(-15);
});
});
describe('getUnsupportedRRuleReason', () => {
test('should return message for HOURLY frequency', () => {
const rrule = new RRule({ freq: RRule.HOURLY });
@@ -142,7 +170,7 @@ describe('isTaskOverdue', () => {
// Create a task due "tomorrow" in UTC
const tomorrow = DateTime.now().plus({ days: 1 }).toUTC().toISO()
const habit = createTestHabit(tomorrow)
// Test in various timezones
expect(isTaskOverdue(habit, 'UTC')).toBe(false)
expect(isTaskOverdue(habit, 'America/New_York')).toBe(false)
@@ -597,7 +625,7 @@ describe('isHabitDueToday', () => {
test('should return false for invalid recurrence rule', () => {
const habit = testHabit('INVALID_RRULE')
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {})
const consoleSpy = spyOn(console, 'error').mockImplementation(() => { })
expect(isHabitDueToday({ habit, timezone: 'UTC' })).toBe(false)
})
})
@@ -710,7 +738,7 @@ describe('isHabitDue', () => {
test('should return false for invalid recurrence rule', () => {
const habit = testHabit('INVALID_RRULE')
const date = DateTime.fromISO('2024-01-01T00:00:00Z')
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {})
const consoleSpy = spyOn(console, 'error').mockImplementation(() => { })
expect(isHabitDue({ habit, timezone: 'UTC', date })).toBe(false)
})
})

View File

@@ -19,6 +19,11 @@ export function getTodayInTimezone(timezone: string): string {
return getISODate({ dateTime: now, timezone });
}
// round a number to the nearest integer
export function roundToInteger(value: number): number {
return Math.round(value);
}
export function getISODate({ dateTime, timezone }: { dateTime: DateTime, timezone: string }): string {
return dateTime.setZone(timezone).toISODate()!;
}
@@ -518,14 +523,19 @@ export function prepareDataForHashing(
* @param dataString The string to hash.
* @returns A promise that resolves to the hex string of the hash.
*/
export async function generateCryptoHash(dataString: string): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(dataString);
// globalThis.crypto should be available in modern browsers and Node.js (v19+)
// For Node.js v15-v18, you might need: const { subtle } = require('node:crypto').webcrypto;
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
// Convert buffer to hex string
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
export async function generateCryptoHash(dataString: string): Promise<string | null> {
try {
const encoder = new TextEncoder();
const data = encoder.encode(dataString);
// globalThis.crypto should be available in modern browsers and Node.js (v19+)
// For Node.js v15-v18, you might need: const { subtle } = require('node:crypto').webcrypto;
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data);
const hashArray = Array.from(new Uint8Array(hashBuffer));
// Convert buffer to hex string
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
} catch (error) {
console.error(`Failed to generate hash: ${error}`);
return null;
}
}