mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
fix: refactored code & removed unused parts
This commit is contained in:
12
lib/atoms.ts
12
lib/atoms.ts
@@ -4,10 +4,11 @@ import {
|
||||
calculateTotalEarned,
|
||||
calculateTotalSpent,
|
||||
calculateTransactionsToday,
|
||||
generateCryptoHash,
|
||||
getCompletionsForToday,
|
||||
getHabitFreq,
|
||||
getTodayInTimezone,
|
||||
isHabitDue,
|
||||
prepareDataForHashing,
|
||||
roundToInteger,
|
||||
t2d
|
||||
} from "@/lib/utils";
|
||||
@@ -121,8 +122,6 @@ export const pomodoroAtom = atom<PomodoroAtom>({
|
||||
minimized: false,
|
||||
})
|
||||
|
||||
import { generateCryptoHash, prepareDataForHashing } from '@/lib/utils';
|
||||
|
||||
export const userSelectAtom = atom<boolean>(false)
|
||||
export const aboutOpenAtom = atom<boolean>(false)
|
||||
|
||||
@@ -229,10 +228,3 @@ export const habitsByDateFamily = atomFamily((dateString: string) =>
|
||||
return habits.filter(habit => isHabitDue({ habit, timezone, date }));
|
||||
})
|
||||
);
|
||||
|
||||
// Derived atom for daily habits
|
||||
export const dailyHabitsAtom = atom((get) => {
|
||||
const settings = get(settingsAtom);
|
||||
const today = getTodayInTimezone(settings.system.timezone);
|
||||
return get(habitsByDateFamily(today));
|
||||
});
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { useAtom } from 'jotai'
|
||||
import { useSession } from "next-auth/react"
|
||||
import { usersAtom } from './atoms'
|
||||
import { checkPermission } from './utils'
|
||||
import { hasPermission } from './utils'
|
||||
|
||||
export function useHelpers() {
|
||||
const { data: session, status } = useSession()
|
||||
@@ -30,8 +30,7 @@ export function useHelpers() {
|
||||
currentUser,
|
||||
usersData,
|
||||
status,
|
||||
hasPermission: (resource: 'habit' | 'wishlist' | 'coins', action: 'write' | 'interact') => currentUser?.isAdmin ||
|
||||
checkPermission(currentUser?.permissions, resource, action),
|
||||
hasPermission,
|
||||
isIOS: iOS(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { RRule } from "rrule"
|
||||
import { uuid } from "./utils"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
export type UserId = string
|
||||
@@ -100,7 +99,7 @@ export interface WishlistData {
|
||||
export const getDefaultUsersData = (): UserData => ({
|
||||
users: [
|
||||
{
|
||||
id: uuid(),
|
||||
id: crypto.randomUUID(),
|
||||
username: 'admin',
|
||||
// password: '', // No default password for admin initially? Or set a secure default?
|
||||
isAdmin: true,
|
||||
|
||||
@@ -3,12 +3,9 @@ import {
|
||||
cn,
|
||||
getTodayInTimezone,
|
||||
getNow,
|
||||
getNowInMilliseconds,
|
||||
t2d,
|
||||
d2t,
|
||||
d2s,
|
||||
d2sDate,
|
||||
d2n,
|
||||
isSameDate,
|
||||
calculateCoinsEarnedToday,
|
||||
calculateTotalEarned,
|
||||
@@ -16,16 +13,15 @@ import {
|
||||
calculateCoinsSpentToday,
|
||||
isHabitDueToday,
|
||||
isHabitDue,
|
||||
uuid,
|
||||
isTaskOverdue,
|
||||
deserializeRRule,
|
||||
serializeRRule,
|
||||
convertHumanReadableFrequencyToMachineReadable,
|
||||
convertMachineReadableFrequencyToHumanReadable,
|
||||
prepareDataForHashing,
|
||||
generateCryptoHash,
|
||||
getUnsupportedRRuleReason,
|
||||
roundToInteger
|
||||
roundToInteger,
|
||||
generateCryptoHash
|
||||
} from './utils'
|
||||
import { CoinTransaction, ParsedResultType, Settings, HabitsData, CoinsData, WishlistData, UserData } from './types'
|
||||
import { DateTime } from "luxon";
|
||||
@@ -178,32 +174,6 @@ describe('isTaskOverdue', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('uuid', () => {
|
||||
test('should generate valid UUIDs', () => {
|
||||
const id = uuid()
|
||||
// UUID v4 format: 8-4-4-4-12 hex digits
|
||||
expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)
|
||||
})
|
||||
|
||||
test('should generate unique UUIDs', () => {
|
||||
const ids = new Set()
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
ids.add(uuid())
|
||||
}
|
||||
// All 1000 UUIDs should be unique
|
||||
expect(ids.size).toBe(1000)
|
||||
})
|
||||
|
||||
test('should generate v4 UUIDs', () => {
|
||||
const id = uuid()
|
||||
// Version 4 UUID has specific bits set:
|
||||
// - 13th character is '4'
|
||||
// - 17th character is '8', '9', 'a', or 'b'
|
||||
expect(id.charAt(14)).toBe('4')
|
||||
expect('89ab').toContain(id.charAt(19))
|
||||
})
|
||||
})
|
||||
|
||||
describe('datetime utilities', () => {
|
||||
let fixedNow: DateTime;
|
||||
let currentDateIndex = 0;
|
||||
@@ -321,13 +291,6 @@ describe('getNow', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNowInMilliseconds', () => {
|
||||
test('should return current time in milliseconds', () => {
|
||||
const now = DateTime.now().setZone('UTC')
|
||||
expect(getNowInMilliseconds()).toBe(now.toMillis().toString())
|
||||
})
|
||||
})
|
||||
|
||||
describe('timestamp conversion utilities', () => {
|
||||
const testTimestamp = '2024-01-01T00:00:00.000Z';
|
||||
const testDateTime = DateTime.fromISO(testTimestamp);
|
||||
@@ -351,16 +314,6 @@ describe('timestamp conversion utilities', () => {
|
||||
const customFormat = d2s({ dateTime: testDateTime, format: 'yyyy-MM-dd', timezone: 'utc' });
|
||||
expect(customFormat).toBe('2024-01-01')
|
||||
})
|
||||
|
||||
test('d2sDate should format DateTime as date string', () => {
|
||||
const result = d2sDate({ dateTime: testDateTime });
|
||||
expect(result).toBeString()
|
||||
})
|
||||
|
||||
test('d2n should convert DateTime to milliseconds string', () => {
|
||||
const result = d2n({ dateTime: testDateTime });
|
||||
expect(result).toBe('1704067200000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isSameDate', () => {
|
||||
|
||||
128
lib/utils.ts
128
lib/utils.ts
@@ -1,13 +1,12 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { DateTime, DateTimeFormatOptions } from "luxon"
|
||||
import { datetime, RRule } from 'rrule'
|
||||
import { Freq, Habit, CoinTransaction, Permission, ParsedFrequencyResult, ParsedResultType, User, Settings, HabitsData, CoinsData, WishlistData, UserData } from '@/lib/types'
|
||||
import { DUE_MAP, INITIAL_DUE, RECURRENCE_RULE_MAP } from "./constants"
|
||||
import { toast } from "@/hooks/use-toast"
|
||||
import { CoinsData, CoinTransaction, Freq, Habit, HabitsData, ParsedFrequencyResult, ParsedResultType, SafeUser, Settings, User, UserData, WishlistData } from '@/lib/types'
|
||||
import * as chrono from 'chrono-node'
|
||||
import _ from "lodash"
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import stableStringify from 'json-stable-stringify';
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { DateTime, DateTimeFormatOptions } from "luxon"
|
||||
import { Formats } from "next-intl"
|
||||
import { datetime, RRule } from 'rrule'
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import { DUE_MAP, INITIAL_DUE, RECURRENCE_RULE_MAP } from "./constants"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
@@ -33,12 +32,6 @@ export function getNow({ timezone = 'utc', keepLocalTime }: { timezone?: string,
|
||||
return DateTime.now().setZone(timezone, { keepLocalTime });
|
||||
}
|
||||
|
||||
// get current time in epoch milliseconds
|
||||
export function getNowInMilliseconds() {
|
||||
const now = getNow({});
|
||||
return d2n({ dateTime: now });
|
||||
}
|
||||
|
||||
// iso timestamp to datetime object, most for storage read
|
||||
export function t2d({ timestamp, timezone }: { timestamp: string; timezone: string }) {
|
||||
return DateTime.fromISO(timestamp).setZone(timezone);
|
||||
@@ -61,30 +54,11 @@ export function d2s({ dateTime, format, timezone }: { dateTime: DateTime, format
|
||||
return dateTime.setZone(timezone).toLocaleString(DateTime.DATETIME_MED);
|
||||
}
|
||||
|
||||
// convert datetime object to date string, mostly for display
|
||||
export function d2sDate({ dateTime }: { dateTime: DateTime }) {
|
||||
return dateTime.toLocaleString(DateTime.DATE_MED);
|
||||
}
|
||||
|
||||
// convert datetime object to epoch milliseconds string, mostly for storage write
|
||||
export function d2n({ dateTime }: { dateTime: DateTime }) {
|
||||
return dateTime.toMillis().toString();
|
||||
}
|
||||
|
||||
// compare the date portion of two datetime objects (i.e. same year, month, day)
|
||||
export function isSameDate(a: DateTime, b: DateTime) {
|
||||
return a.hasSame(b, 'day');
|
||||
}
|
||||
|
||||
export function normalizeCompletionDate(date: string, timezone: string): string {
|
||||
// If already in ISO format, return as is
|
||||
if (date.includes('T')) {
|
||||
return date;
|
||||
}
|
||||
// Convert from yyyy-MM-dd to ISO format
|
||||
return DateTime.fromFormat(date, 'yyyy-MM-dd', { zone: timezone }).toUTC().toISO()!;
|
||||
}
|
||||
|
||||
export function getCompletionsForDate({
|
||||
habit,
|
||||
date,
|
||||
@@ -438,22 +412,20 @@ export const openWindow = (url: string): boolean => {
|
||||
return true
|
||||
}
|
||||
|
||||
export function deepMerge<T>(a: T, b: T) {
|
||||
return _.merge(a, b, (x: unknown, y: unknown) => {
|
||||
if (_.isArray(a)) {
|
||||
return a.concat(b)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function checkPermission(
|
||||
permissions: Permission[] | undefined,
|
||||
export function hasPermission(
|
||||
user: User | undefined,
|
||||
resource: 'habit' | 'wishlist' | 'coins',
|
||||
action: 'write' | 'interact'
|
||||
): boolean {
|
||||
if (!permissions) return false
|
||||
|
||||
return permissions.some(permission => {
|
||||
if (!user || !user.permissions) {
|
||||
return false;
|
||||
}
|
||||
// If user is admin, they have all permissions.
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise, check specific permissions.
|
||||
return user.permissions.some(permission => {
|
||||
switch (resource) {
|
||||
case 'habit':
|
||||
return permission.habit[action]
|
||||
@@ -467,27 +439,6 @@ export function checkPermission(
|
||||
})
|
||||
}
|
||||
|
||||
export function uuid() {
|
||||
return uuidv4()
|
||||
}
|
||||
|
||||
export function hasPermission(
|
||||
currentUser: User | undefined,
|
||||
resource: 'habit' | 'wishlist' | 'coins',
|
||||
action: 'write' | 'interact'
|
||||
): boolean {
|
||||
// If no current user, no permissions.
|
||||
if (!currentUser) {
|
||||
return false;
|
||||
}
|
||||
// If user is admin, they have all permissions.
|
||||
if (currentUser.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
// Otherwise, check specific permissions.
|
||||
return checkPermission(currentUser.permissions, resource, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares a consistent string representation of the data for hashing.
|
||||
* It combines all relevant data pieces into a single object and then stringifies it stably.
|
||||
@@ -499,22 +450,13 @@ export function prepareDataForHashing(
|
||||
wishlist: WishlistData,
|
||||
users: UserData
|
||||
): string {
|
||||
// Combine all data into a single object.
|
||||
// The order of keys in this object itself doesn't matter due to stableStringify,
|
||||
// but being explicit helps in understanding what's being hashed.
|
||||
const combinedData = {
|
||||
return JSON.stringify({
|
||||
settings,
|
||||
habits,
|
||||
coins,
|
||||
wishlist,
|
||||
users,
|
||||
};
|
||||
const stringifiedData = stableStringify(combinedData);
|
||||
// Handle cases where stringify might return undefined.
|
||||
if (stringifiedData === undefined) {
|
||||
throw new Error("Failed to stringify data for hashing. stableStringify returned undefined.");
|
||||
}
|
||||
return stringifiedData;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -539,3 +481,31 @@ export async function generateCryptoHash(dataString: string): Promise<string | n
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function handlePermissionCheck(
|
||||
user: User | SafeUser | undefined,
|
||||
resource: 'habit' | 'wishlist' | 'coins',
|
||||
action: 'write' | 'interact',
|
||||
tCommon: (key: string, values?: Record<string, string | number | Date> | undefined, formats?: Formats | undefined) => string
|
||||
): boolean {
|
||||
|
||||
if (!user) {
|
||||
toast({
|
||||
title: tCommon("authenticationRequiredTitle"),
|
||||
description: tCommon("authenticationRequiredDescription"),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
if (!hasPermission(user, resource, action)) {
|
||||
toast({
|
||||
title: tCommon("permissionDeniedTitle"),
|
||||
description: tCommon("permissionDeniedDescription", { action, resource }),
|
||||
variant: "destructive",
|
||||
})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user