Skip to content

Commit 8d2bfaf

Browse files
authored
update PWA icon, fix floating number balance (#159)
1 parent 98b5d5e commit 8d2bfaf

File tree

12 files changed

+100
-32
lines changed

12 files changed

+100
-32
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,5 @@ next-env.d.ts
4646
Budfile
4747
certificates
4848
/backups/*
49+
50+
CHANGELOG.md.tmp

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## Version 0.2.23
4+
5+
### Fixed
6+
7+
* floating number coin balance (#155)
8+
* disable freshness check if browser does not support web crypto (#161)
9+
10+
### Improved
11+
12+
* use transparent background PWA icon with correct text (#103)
13+
* display icon in logo
14+
315
## Version 0.2.22
416

517
### Added

app/actions/data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ async function saveData<T>(type: DataType, data: T): Promise<void> {
130130
* Calculates the server's global freshness token based on all core data files.
131131
* This is an expensive operation as it reads all data files.
132132
*/
133-
async function calculateServerFreshnessToken(): Promise<string> {
133+
async function calculateServerFreshnessToken(): Promise<string | null> {
134134
try {
135135
const settings = await loadSettings();
136136
const habits = await loadHabitsData();

components/Logo.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import { Sparkles } from "lucide-react"
1+
import Image from "next/image"
22

33
export function Logo() {
44
return (
55
<div className="flex items-center gap-2">
6-
{/* <Sparkles className="h-6 w-6 text-primary" /> */}
6+
<Image src="/icons/icon.png" alt="HabitTrove Logo" width={96} height={96} className="h-12 w-12 hidden xs:inline" />
77
<span className="font-bold text-xl">HabitTrove</span>
88
</div>
99
)

hooks/useCoins.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { useAtom } from 'jotai';
22
import { useState, useEffect, useMemo } from 'react';
33
import { useTranslations } from 'next-intl';
4-
import { calculateCoinsEarnedToday, calculateCoinsSpentToday, calculateTotalEarned, calculateTotalSpent, calculateTransactionsToday, checkPermission } from '@/lib/utils'
4+
import { calculateCoinsEarnedToday, calculateCoinsSpentToday, calculateTotalEarned, calculateTotalSpent, calculateTransactionsToday, checkPermission, roundToInteger } from '@/lib/utils'
55
import {
66
coinsAtom,
77
coinsEarnedTodayAtom,
@@ -86,12 +86,22 @@ export function useCoins(options?: { selectedUser?: string }) {
8686
setBalance(loggedInUserBalance);
8787
} else if (targetUser?.id) {
8888
// If an admin is viewing another user, calculate their metrics manually
89-
setCoinsEarnedToday(calculateCoinsEarnedToday(transactions, timezone));
90-
setTotalEarned(calculateTotalEarned(transactions));
91-
setTotalSpent(calculateTotalSpent(transactions));
92-
setCoinsSpentToday(calculateCoinsSpentToday(transactions, timezone));
93-
setTransactionsToday(calculateTransactionsToday(transactions, timezone));
94-
setBalance(transactions.reduce((acc, t) => acc + t.amount, 0));
89+
const earnedToday = calculateCoinsEarnedToday(transactions, timezone);
90+
setCoinsEarnedToday(roundToInteger(earnedToday));
91+
92+
const totalEarnedVal = calculateTotalEarned(transactions);
93+
setTotalEarned(roundToInteger(totalEarnedVal));
94+
95+
const totalSpentVal = calculateTotalSpent(transactions);
96+
setTotalSpent(roundToInteger(totalSpentVal));
97+
98+
const spentToday = calculateCoinsSpentToday(transactions, timezone);
99+
setCoinsSpentToday(roundToInteger(spentToday));
100+
101+
setTransactionsToday(calculateTransactionsToday(transactions, timezone)); // This is a count
102+
103+
const calculatedBalance = transactions.reduce((acc, t) => acc + t.amount, 0);
104+
setBalance(roundToInteger(calculatedBalance));
95105
}
96106
}, [
97107
targetUser?.id,

lib/atoms.ts

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ import {
2626
isHabitDueToday,
2727
getNow,
2828
isHabitDue,
29-
getHabitFreq
29+
getHabitFreq,
30+
roundToInteger
3031
} from "@/lib/utils";
3132
import { atomFamily, atomWithStorage } from "jotai/utils";
3233
import { DateTime } from "luxon";
@@ -57,26 +58,30 @@ export const serverSettingsAtom = atom(getDefaultServerSettings());
5758
export const coinsEarnedTodayAtom = atom((get) => {
5859
const coins = get(coinsAtom);
5960
const settings = get(settingsAtom);
60-
return calculateCoinsEarnedToday(coins.transactions, settings.system.timezone);
61+
const value = calculateCoinsEarnedToday(coins.transactions, settings.system.timezone);
62+
return roundToInteger(value);
6163
});
6264

6365
// Derived atom for total earned
6466
export const totalEarnedAtom = atom((get) => {
6567
const coins = get(coinsAtom);
66-
return calculateTotalEarned(coins.transactions);
68+
const value = calculateTotalEarned(coins.transactions);
69+
return roundToInteger(value);
6770
});
6871

6972
// Derived atom for total spent
7073
export const totalSpentAtom = atom((get) => {
7174
const coins = get(coinsAtom);
72-
return calculateTotalSpent(coins.transactions);
75+
const value = calculateTotalSpent(coins.transactions);
76+
return roundToInteger(value);
7377
});
7478

7579
// Derived atom for coins spent today
7680
export const coinsSpentTodayAtom = atom((get) => {
7781
const coins = get(coinsAtom);
7882
const settings = get(settingsAtom);
79-
return calculateCoinsSpentToday(coins.transactions, settings.system.timezone);
83+
const value = calculateCoinsSpentToday(coins.transactions, settings.system.timezone);
84+
return roundToInteger(value);
8085
});
8186

8287
// Derived atom for transactions today
@@ -103,9 +108,10 @@ export const coinsBalanceAtom = atom((get) => {
103108
return 0; // No user logged in or ID not set, so balance is 0
104109
}
105110
const coins = get(coinsAtom);
106-
return coins.transactions
111+
const balance = coins.transactions
107112
.filter(transaction => transaction.userId === loggedInUserId)
108113
.reduce((sum, transaction) => sum + transaction.amount, 0);
114+
return roundToInteger(balance);
109115
});
110116

111117
/* transient atoms */

lib/utils.test.ts

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ import {
2222
serializeRRule,
2323
convertHumanReadableFrequencyToMachineReadable,
2424
convertMachineReadableFrequencyToHumanReadable,
25-
getUnsupportedRRuleReason,
2625
prepareDataForHashing,
27-
generateCryptoHash
26+
generateCryptoHash,
27+
getUnsupportedRRuleReason,
28+
roundToInteger
2829
} from './utils'
2930
import { CoinTransaction, ParsedResultType, Settings, HabitsData, CoinsData, WishlistData, UserData } from './types'
3031
import { DateTime } from "luxon";
@@ -42,6 +43,33 @@ describe('cn utility', () => {
4243
})
4344
})
4445

46+
describe('roundToInteger', () => {
47+
test('should round positive numbers correctly', () => {
48+
expect(roundToInteger(10.123)).toBe(10);
49+
expect(roundToInteger(10.5)).toBe(11);
50+
expect(roundToInteger(10.75)).toBe(11);
51+
expect(roundToInteger(10.49)).toBe(10);
52+
});
53+
54+
test('should round negative numbers correctly', () => {
55+
expect(roundToInteger(-10.123)).toBe(-10);
56+
expect(roundToInteger(-10.5)).toBe(-10); // Math.round rounds -x.5 to -(x-1) e.g. -10.5 to -10
57+
expect(roundToInteger(-10.75)).toBe(-11);
58+
expect(roundToInteger(-10.49)).toBe(-10);
59+
});
60+
61+
test('should handle zero correctly', () => {
62+
expect(roundToInteger(0)).toBe(0);
63+
expect(roundToInteger(0.0)).toBe(0);
64+
expect(roundToInteger(-0.0)).toBe(-0);
65+
});
66+
67+
test('should handle integers correctly', () => {
68+
expect(roundToInteger(15)).toBe(15);
69+
expect(roundToInteger(-15)).toBe(-15);
70+
});
71+
});
72+
4573
describe('getUnsupportedRRuleReason', () => {
4674
test('should return message for HOURLY frequency', () => {
4775
const rrule = new RRule({ freq: RRule.HOURLY });
@@ -142,7 +170,7 @@ describe('isTaskOverdue', () => {
142170
// Create a task due "tomorrow" in UTC
143171
const tomorrow = DateTime.now().plus({ days: 1 }).toUTC().toISO()
144172
const habit = createTestHabit(tomorrow)
145-
173+
146174
// Test in various timezones
147175
expect(isTaskOverdue(habit, 'UTC')).toBe(false)
148176
expect(isTaskOverdue(habit, 'America/New_York')).toBe(false)
@@ -597,7 +625,7 @@ describe('isHabitDueToday', () => {
597625

598626
test('should return false for invalid recurrence rule', () => {
599627
const habit = testHabit('INVALID_RRULE')
600-
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {})
628+
const consoleSpy = spyOn(console, 'error').mockImplementation(() => { })
601629
expect(isHabitDueToday({ habit, timezone: 'UTC' })).toBe(false)
602630
})
603631
})
@@ -710,7 +738,7 @@ describe('isHabitDue', () => {
710738
test('should return false for invalid recurrence rule', () => {
711739
const habit = testHabit('INVALID_RRULE')
712740
const date = DateTime.fromISO('2024-01-01T00:00:00Z')
713-
const consoleSpy = spyOn(console, 'error').mockImplementation(() => {})
741+
const consoleSpy = spyOn(console, 'error').mockImplementation(() => { })
714742
expect(isHabitDue({ habit, timezone: 'UTC', date })).toBe(false)
715743
})
716744
})

lib/utils.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ export function getTodayInTimezone(timezone: string): string {
1919
return getISODate({ dateTime: now, timezone });
2020
}
2121

22+
// round a number to the nearest integer
23+
export function roundToInteger(value: number): number {
24+
return Math.round(value);
25+
}
26+
2227
export function getISODate({ dateTime, timezone }: { dateTime: DateTime, timezone: string }): string {
2328
return dateTime.setZone(timezone).toISODate()!;
2429
}
@@ -518,14 +523,19 @@ export function prepareDataForHashing(
518523
* @param dataString The string to hash.
519524
* @returns A promise that resolves to the hex string of the hash.
520525
*/
521-
export async function generateCryptoHash(dataString: string): Promise<string> {
522-
const encoder = new TextEncoder();
523-
const data = encoder.encode(dataString);
524-
// globalThis.crypto should be available in modern browsers and Node.js (v19+)
525-
// For Node.js v15-v18, you might need: const { subtle } = require('node:crypto').webcrypto;
526-
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data);
527-
const hashArray = Array.from(new Uint8Array(hashBuffer));
528-
// Convert buffer to hex string
529-
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
530-
return hashHex;
526+
export async function generateCryptoHash(dataString: string): Promise<string | null> {
527+
try {
528+
const encoder = new TextEncoder();
529+
const data = encoder.encode(dataString);
530+
// globalThis.crypto should be available in modern browsers and Node.js (v19+)
531+
// For Node.js v15-v18, you might need: const { subtle } = require('node:crypto').webcrypto;
532+
const hashBuffer = await globalThis.crypto.subtle.digest('SHA-256', data);
533+
const hashArray = Array.from(new Uint8Array(hashBuffer));
534+
// Convert buffer to hex string
535+
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
536+
return hashHex;
537+
} catch (error) {
538+
console.error(`Failed to generate hash: ${error}`);
539+
return null;
540+
}
531541
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "habittrove",
3-
"version": "0.2.22",
3+
"version": "0.2.23",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack",

public/icons/icon.png

-1.31 KB
Loading

0 commit comments

Comments
 (0)