mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
refactor: replace moment library with luxon
This commit is contained in:
8
Budfile
8
Budfile
@@ -93,3 +93,11 @@ docker_push() {
|
||||
echo "Pushed Docker images with tags: v$version"
|
||||
fi
|
||||
}
|
||||
|
||||
run() {
|
||||
npm run dev
|
||||
}
|
||||
|
||||
build() {
|
||||
npm run build
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
DataType,
|
||||
DATA_DEFAULTS
|
||||
} from '@/lib/types'
|
||||
import { d2t, getNow, getNowInMilliseconds } from '@/lib/utils';
|
||||
|
||||
function getDefaultData<T>(type: DataType): T {
|
||||
return DATA_DEFAULTS[type]() as T;
|
||||
@@ -106,7 +107,7 @@ export async function addCoins(
|
||||
amount,
|
||||
type,
|
||||
description,
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp: d2t({ dateTime: getNow({}) }),
|
||||
...(relatedItemId && { relatedItemId })
|
||||
}
|
||||
|
||||
@@ -154,7 +155,7 @@ export async function removeCoins(
|
||||
amount: -amount,
|
||||
type,
|
||||
description,
|
||||
timestamp: new Date().toISOString(),
|
||||
timestamp: d2t({ dateTime: getNow({}) }),
|
||||
...(relatedItemId && { relatedItemId })
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
import { getDateInTimezone } from '@/lib/utils'
|
||||
import { t2d, d2s, getNow, isSameDate } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { formatNumber } from '@/lib/utils/formatNumber'
|
||||
import { History } from 'lucide-react'
|
||||
@@ -145,8 +145,7 @@ export default function CoinsManager() {
|
||||
<div className="text-sm text-blue-800 dark:text-blue-100 mb-1">Today's Transactions</div>
|
||||
<div className="text-2xl font-bold text-blue-900 dark:text-blue-50">
|
||||
{transactions.filter(t =>
|
||||
getDateInTimezone(t.timestamp, settings.system.timezone).toDateString() ===
|
||||
getDateInTimezone(new Date(), settings.system.timezone).toDateString()
|
||||
isSameDate(getNow({}), t2d({ timestamp: t.timestamp }))
|
||||
).length} 📊
|
||||
</div>
|
||||
</div>
|
||||
@@ -213,7 +212,7 @@ export default function CoinsManager() {
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
{getDateInTimezone(transaction.timestamp, settings.system.timezone).toLocaleString()}
|
||||
{d2s({ dateTime: t2d({ timestamp: transaction.timestamp, timezone: settings.system.timezone }) })}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import moment from 'moment-timezone'
|
||||
import { DateTime } from 'luxon'
|
||||
import { d2s } from '@/lib/utils'
|
||||
|
||||
interface DynamicTimeProps {
|
||||
timezone: string
|
||||
}
|
||||
|
||||
export function DynamicTime({ timezone }: DynamicTimeProps) {
|
||||
const [time, setTime] = useState(moment())
|
||||
const [time, setTime] = useState(DateTime.now().setZone(timezone))
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTime(moment())
|
||||
setTime((prevTime) => prevTime.plus({ seconds: 1 }))
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
@@ -20,7 +21,7 @@ export function DynamicTime({ timezone }: DynamicTimeProps) {
|
||||
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{time.tz(timezone).format('dddd, MMMM D, YYYY h:mm:ss A')}
|
||||
{d2s({ dateTime: time })}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,15 +7,25 @@ import { Badge } from '@/components/ui/badge'
|
||||
|
||||
import { loadHabitsData } from '@/app/actions/data'
|
||||
import { Habit } from '@/lib/types'
|
||||
import { d2s, getNow } from '@/lib/utils'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
import { DateTime } from 'luxon'
|
||||
|
||||
export default function HabitCalendar() {
|
||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(new Date())
|
||||
const { settings } = useSettings()
|
||||
const [selectedDate, setSelectedDate] = useState<DateTime>(getNow({ timezone: settings.system.timezone }))
|
||||
const [habits, setHabits] = useState<Habit[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
fetchHabitsData()
|
||||
}, [])
|
||||
|
||||
// Update selectedDate when timezone changes
|
||||
useEffect(() => {
|
||||
const now = getNow({ timezone: settings.system.timezone })
|
||||
setSelectedDate(now)
|
||||
}, [settings])
|
||||
|
||||
const fetchHabitsData = async () => {
|
||||
const data = await loadHabitsData()
|
||||
setHabits(data.habits)
|
||||
@@ -39,8 +49,8 @@ export default function HabitCalendar() {
|
||||
<CardContent>
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={selectedDate}
|
||||
onSelect={setSelectedDate}
|
||||
selected={selectedDate.toJSDate()}
|
||||
onSelect={(e) => e && setSelectedDate(DateTime.fromJSDate(e))}
|
||||
className="rounded-md border"
|
||||
modifiers={{
|
||||
completed: (date) => getHabitsForDate(date).length > 0,
|
||||
@@ -55,7 +65,7 @@ export default function HabitCalendar() {
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{selectedDate ? (
|
||||
<>Habits for {selectedDate.toLocaleDateString()}</>
|
||||
<>Habits for {d2s({ dateTime: selectedDate })}</>
|
||||
) : (
|
||||
'Select a date'
|
||||
)}
|
||||
@@ -65,7 +75,7 @@ export default function HabitCalendar() {
|
||||
{selectedDate && (
|
||||
<ul className="space-y-2">
|
||||
{habits.map((habit) => {
|
||||
const isCompleted = getHabitsForDate(selectedDate).some(h => h.id === habit.id)
|
||||
const isCompleted = getHabitsForDate(selectedDate.toJSDate()).some(h => h.id === habit.id)
|
||||
return (
|
||||
<li key={habit.id} className="flex items-center justify-between">
|
||||
<span>{habit.name}</span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import HeatMap from '@uiw/react-heat-map'
|
||||
import { Habit } from '@/lib/types'
|
||||
import { getDateInTimezone } from '@/lib/utils'
|
||||
import { getNow } from '@/lib/utils'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
|
||||
interface HabitHeatmapProps {
|
||||
@@ -29,9 +29,8 @@ export default function HabitHeatmap({ habits }: HabitHeatmapProps) {
|
||||
const { settings } = useSettings()
|
||||
|
||||
// Get start date (30 days ago)
|
||||
const now = getDateInTimezone(new Date(), settings.system.timezone)
|
||||
const startDate = now
|
||||
startDate.setDate(now.getDate() - 30)
|
||||
const now = getNow({ timezone: settings.system.timezone })
|
||||
const startDate = now.minus({ days: 30 }).toJSDate()
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-white rounded-lg shadow">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Habit } from '@/lib/types'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
import { getDateInTimezone } from '@/lib/utils'
|
||||
import { d2s, getNow } from '@/lib/utils'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
|
||||
interface HabitStreakProps {
|
||||
@@ -12,20 +12,21 @@ interface HabitStreakProps {
|
||||
|
||||
export default function HabitStreak({ habits }: HabitStreakProps) {
|
||||
const { settings } = useSettings()
|
||||
// Get the last 30 days of data
|
||||
const dates = Array.from({ length: 30 }, (_, i) => {
|
||||
const d = getDateInTimezone(new Date(), settings.system.timezone)
|
||||
d.setDate(d.getDate() - i)
|
||||
return d.toISOString().split('T')[0]
|
||||
// Get the last 7 days of data
|
||||
const dates = Array.from({ length: 7 }, (_, i) => {
|
||||
const d = getNow({ timezone: settings.system.timezone });
|
||||
return d2s({ dateTime: d.minus({ days: i }), format: 'yyyy-MM-dd' });
|
||||
}).reverse()
|
||||
|
||||
// Count completed habits per day
|
||||
const completions = dates.map(date => ({
|
||||
date: new Date(date).toLocaleDateString(),
|
||||
completed: habits.filter(habit =>
|
||||
habit.completions.includes(date)
|
||||
).length
|
||||
}))
|
||||
const completions = dates.map(date => {
|
||||
const completedCount = habits.reduce((count, habit) => {
|
||||
return count + (habit.completions.includes(date) ? 1 : 0);
|
||||
}, 0);
|
||||
return {
|
||||
date,
|
||||
completed: completedCount
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { toast } from '@/hooks/use-toast'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { Undo2 } from 'lucide-react'
|
||||
import { Habit } from '@/lib/types'
|
||||
import { getDateInTimezone, getTodayInTimezone } from '@/lib/utils'
|
||||
import { getNowInMilliseconds, getTodayInTimezone } from '@/lib/utils'
|
||||
|
||||
export function useHabits() {
|
||||
const [habits, setHabits] = useState<Habit[]>([])
|
||||
@@ -21,7 +21,7 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
const addHabit = async (habit: Omit<Habit, 'id'>) => {
|
||||
const newHabit = { ...habit, id: getDateInTimezone(new Date(), settings.system.timezone).getTime().toString() }
|
||||
const newHabit = { ...habit, id: getNowInMilliseconds() }
|
||||
const newHabits = [...habits, newHabit]
|
||||
setHabits(newHabits)
|
||||
await saveHabitsData({ habits: newHabits })
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, test, describe, beforeAll, afterAll } from "bun:test";
|
||||
import { cn, getDateInTimezone, getTodayInTimezone } from './utils'
|
||||
import { cn, getTodayInTimezone, getNow, getNowInMilliseconds, t2d, d2t, d2s, d2sDate, d2n, isSameDate } from './utils'
|
||||
import { DateTime } from "luxon";
|
||||
|
||||
describe('cn utility', () => {
|
||||
test('should merge class names correctly', () => {
|
||||
@@ -10,41 +11,86 @@ describe('cn utility', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('timezone utilities', () => {
|
||||
describe('getDateInTimezone', () => {
|
||||
test('should convert date to specified timezone', () => {
|
||||
const date = new Date('2024-01-01T00:00:00Z')
|
||||
describe('datetime utilities', () => {
|
||||
let fixedNow: DateTime;
|
||||
|
||||
// Test with specific timezones
|
||||
const nyDate = getDateInTimezone(date, 'America/New_York')
|
||||
expect(nyDate.toISOString()).toBe('2023-12-31T19:00:00.000Z') // NY is UTC-5
|
||||
|
||||
const tokyoDate = getDateInTimezone(date, 'Asia/Tokyo')
|
||||
expect(tokyoDate.toISOString()).toBe('2024-01-01T09:00:00.000Z') // Tokyo is UTC+9
|
||||
})
|
||||
|
||||
test('should handle string dates', () => {
|
||||
const dateStr = '2024-01-01T00:00:00Z'
|
||||
const nyDate = getDateInTimezone(dateStr, 'America/New_York')
|
||||
expect(nyDate.toISOString()).toBe('2023-12-31T19:00:00.000Z')
|
||||
})
|
||||
beforeAll(() => {
|
||||
// Fix the current time to 2024-01-01T00:00:00Z
|
||||
fixedNow = DateTime.fromISO('2024-01-01T00:00:00Z');
|
||||
DateTime.now = () => fixedNow;
|
||||
})
|
||||
|
||||
describe('getTodayInTimezone', () => {
|
||||
let originalDate: Date;
|
||||
|
||||
beforeAll(() => {
|
||||
originalDate = new Date();
|
||||
globalThis.Date.now = () => new Date('2024-01-01T00:00:00Z').getTime();
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
globalThis.Date.now = () => originalDate.getTime();
|
||||
})
|
||||
|
||||
test('should return today in YYYY-MM-DD format for timezone', () => {
|
||||
expect(getTodayInTimezone('America/New_York')).toBe('2023-12-31')
|
||||
expect(getTodayInTimezone('Asia/Tokyo')).toBe('2024-01-01')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNow', () => {
|
||||
test('should return current datetime in specified timezone', () => {
|
||||
const nyNow = getNow({ timezone: 'America/New_York' });
|
||||
expect(nyNow.zoneName).toBe('America/New_York')
|
||||
expect(nyNow.year).toBe(2023)
|
||||
expect(nyNow.month).toBe(12)
|
||||
expect(nyNow.day).toBe(31)
|
||||
})
|
||||
|
||||
test('should default to UTC', () => {
|
||||
const utcNow = getNow({});
|
||||
expect(utcNow.zoneName).toBe('UTC')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNowInMilliseconds', () => {
|
||||
test('should return current time in milliseconds', () => {
|
||||
expect(getNowInMilliseconds()).toBe('1704067200000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('timestamp conversion utilities', () => {
|
||||
const testTimestamp = '2024-01-01T00:00:00.000Z';
|
||||
const testDateTime = DateTime.fromISO(testTimestamp);
|
||||
|
||||
test('t2d should convert ISO timestamp to DateTime', () => {
|
||||
const result = t2d({ timestamp: testTimestamp });
|
||||
// Normalize both timestamps to handle different UTC offset formats (Z vs +00:00)
|
||||
expect(DateTime.fromISO(result.toISO()!).toMillis())
|
||||
.toBe(DateTime.fromISO(testTimestamp).toMillis())
|
||||
})
|
||||
|
||||
test('d2t should convert DateTime to ISO timestamp', () => {
|
||||
const result = d2t({ dateTime: testDateTime });
|
||||
expect(result).toBe(testTimestamp)
|
||||
})
|
||||
|
||||
test('d2s should format DateTime for display', () => {
|
||||
const result = d2s({ dateTime: testDateTime });
|
||||
expect(result).toBeString()
|
||||
|
||||
const customFormat = d2s({ dateTime: testDateTime, format: 'yyyy-MM-dd' });
|
||||
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', () => {
|
||||
test('should compare dates correctly', () => {
|
||||
const date1 = DateTime.fromISO('2024-01-01T12:00:00Z');
|
||||
const date2 = DateTime.fromISO('2024-01-01T15:00:00Z');
|
||||
const date3 = DateTime.fromISO('2024-01-02T12:00:00Z');
|
||||
|
||||
expect(isSameDate(date1, date2)).toBe(true)
|
||||
expect(isSameDate(date1, date3)).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
53
lib/utils.ts
53
lib/utils.ts
@@ -1,16 +1,57 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import moment from "moment-timezone"
|
||||
import { DateTime } from "luxon"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
|
||||
export function getDateInTimezone(date: Date | string, timezone: string): Date {
|
||||
const m = moment.tz(date, timezone);
|
||||
return new Date(m.format('YYYY-MM-DDTHH:mm:ss.SSS[Z]'));
|
||||
// get today's date string for timezone
|
||||
export function getTodayInTimezone(timezone: string): string {
|
||||
const now = getNow({ timezone });
|
||||
return d2s({ dateTime: now, format: 'yyyy-MM-dd' });
|
||||
}
|
||||
|
||||
export function getTodayInTimezone(timezone: string): string {
|
||||
return moment.tz(timezone).format('YYYY-MM-DD');
|
||||
// get datetime object of now
|
||||
export function getNow({ timezone = 'utc' }: { timezone?: string }) {
|
||||
return DateTime.now().setZone(timezone);
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// convert datetime object to iso timestamp, mostly for storage write
|
||||
export function d2t({ dateTime, timezone = 'utc' }: { dateTime: DateTime, timezone?: string }) {
|
||||
return dateTime.setZone(timezone).toISO()!;
|
||||
}
|
||||
|
||||
// convert datetime object to string, mostly for display
|
||||
export function d2s({ dateTime, format }: { dateTime: DateTime, format?: string }) {
|
||||
if (format) {
|
||||
return dateTime.toFormat(format);
|
||||
}
|
||||
return dateTime.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');
|
||||
}
|
||||
|
||||
37
package-lock.json
generated
37
package-lock.json
generated
@@ -25,8 +25,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"js-confetti": "^0.12.0",
|
||||
"lucide-react": "^0.469.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.46",
|
||||
"luxon": "^3.5.0",
|
||||
"next": "15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-confetti": "^6.2.2",
|
||||
@@ -40,6 +39,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.17.10",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
@@ -1699,6 +1699,12 @@
|
||||
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
|
||||
"integrity": "sha512-TifLZlFudklWlMBfhubvgqTXRzLDI5pCbGa4P8a3wPyUQSW+1xQ5eDsreP9DWHX3tjq1ke96uYG/nwundroWcA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mdast": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz",
|
||||
@@ -5299,6 +5305,14 @@
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/luxon": {
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz",
|
||||
"integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -5953,25 +5967,6 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/moment": {
|
||||
"version": "2.30.1",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
|
||||
"integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/moment-timezone": {
|
||||
"version": "0.5.46",
|
||||
"resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.46.tgz",
|
||||
"integrity": "sha512-ZXm9b36esbe7OmdABqIWJuBBiLLwAjrN7CE+7sYdCCx82Nabt1wHDj8TVseS59QIlfFPbOoiBPm6ca9BioG4hw==",
|
||||
"dependencies": {
|
||||
"moment": "^2.29.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"js-confetti": "^0.12.0",
|
||||
"lucide-react": "^0.469.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.46",
|
||||
"luxon": "^3.5.0",
|
||||
"next": "15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-confetti": "^6.2.2",
|
||||
@@ -43,6 +42,7 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@tailwindcss/typography": "^0.5.15",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^20.17.10",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
|
||||
Reference in New Issue
Block a user