mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
update PWA icon, fix floating number balance (#159)
This commit is contained in:
18
lib/atoms.ts
18
lib/atoms.ts
@@ -26,7 +26,8 @@ import {
|
||||
isHabitDueToday,
|
||||
getNow,
|
||||
isHabitDue,
|
||||
getHabitFreq
|
||||
getHabitFreq,
|
||||
roundToInteger
|
||||
} from "@/lib/utils";
|
||||
import { atomFamily, atomWithStorage } from "jotai/utils";
|
||||
import { DateTime } from "luxon";
|
||||
@@ -57,26 +58,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
|
||||
@@ -103,9 +108,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 */
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
30
lib/utils.ts
30
lib/utils.ts
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user