mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-20 22:24:28 +01:00
added timezone settings
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## Version 0.1.5
|
||||
|
||||
### Added
|
||||
|
||||
- docker-compose.yaml
|
||||
- timezone settings
|
||||
|
||||
### Fixed
|
||||
|
||||
- completing habits now respect timezone settings
|
||||
|
||||
## Version 0.1.4
|
||||
|
||||
### Changed
|
||||
|
||||
@@ -124,6 +124,9 @@ export async function loadSettings(): Promise<Settings> {
|
||||
ui: {
|
||||
useNumberFormatting: true,
|
||||
useGrouping: true,
|
||||
},
|
||||
system: {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
import { DynamicTimeNoSSR } from '@/components/DynamicTimeNoSSR'
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { settings, updateSettings } = useSettings()
|
||||
@@ -13,7 +14,6 @@ export default function SettingsPage() {
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Settings</h1>
|
||||
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>UI Settings</CardTitle>
|
||||
@@ -58,6 +58,42 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle>System Settings</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="space-y-0.5">
|
||||
<Label htmlFor="timezone">Timezone</Label>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Select your timezone for accurate date tracking
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2">
|
||||
<select
|
||||
id="timezone"
|
||||
value={settings.system.timezone}
|
||||
onChange={(e) =>
|
||||
updateSettings({
|
||||
...settings,
|
||||
system: { ...settings.system, timezone: e.target.value }
|
||||
})
|
||||
}
|
||||
className="w-[200px] rounded-md border border-input bg-background px-3 py-2"
|
||||
>
|
||||
{Intl.supportedValuesOf('timeZone').map((tz) => (
|
||||
<option key={tz} value={tz}>
|
||||
{tz}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<DynamicTimeNoSSR timezone={settings.system.timezone} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
import { getDateInTimezone } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { formatNumber } from '@/lib/utils/formatNumber'
|
||||
import { History } from 'lucide-react'
|
||||
@@ -10,7 +11,6 @@ import { Input } from '@/components/ui/input'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { useCoins } from '@/hooks/useCoins'
|
||||
import { format } from 'date-fns'
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function CoinsManager() {
|
||||
@@ -145,7 +145,8 @@ 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 =>
|
||||
new Date(t.timestamp).toDateString() === new Date().toDateString()
|
||||
getDateInTimezone(t.timestamp, settings.system.timezone).toDateString() ===
|
||||
getDateInTimezone(new Date(), settings.system.timezone).toDateString()
|
||||
).length} 📊
|
||||
</div>
|
||||
</div>
|
||||
@@ -212,7 +213,7 @@ export default function CoinsManager() {
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-500">
|
||||
{format(new Date(transaction.timestamp), 'PPpp')}
|
||||
{getDateInTimezone(transaction.timestamp, settings.system.timezone).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<span
|
||||
@@ -231,6 +232,6 @@ export default function CoinsManager() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
@@ -22,7 +24,8 @@ export default function DailyOverview({
|
||||
onComplete,
|
||||
onUndo
|
||||
}: UpcomingItemsProps) {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const { settings } = useSettings()
|
||||
const today = getTodayInTimezone(settings.system.timezone)
|
||||
const todayCompletions = habits.filter(habit =>
|
||||
habit.completions.includes(today)
|
||||
)
|
||||
|
||||
26
components/DynamicTime.tsx
Normal file
26
components/DynamicTime.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import moment from 'moment-timezone'
|
||||
|
||||
interface DynamicTimeProps {
|
||||
timezone: string
|
||||
}
|
||||
|
||||
export function DynamicTime({ timezone }: DynamicTimeProps) {
|
||||
const [time, setTime] = useState(moment())
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setInterval(() => {
|
||||
setTime(moment())
|
||||
}, 1000)
|
||||
|
||||
return () => clearInterval(timer)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{time.tz(timezone).format('dddd, MMMM D, YYYY h:mm:ss A')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
8
components/DynamicTimeNoSSR.tsx
Normal file
8
components/DynamicTimeNoSSR.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import dynamic from 'next/dynamic'
|
||||
import { DynamicTime } from './DynamicTime'
|
||||
|
||||
const DynamicTimeNoSSR = dynamic(() => Promise.resolve(DynamicTime), {
|
||||
ssr: false
|
||||
})
|
||||
|
||||
export { DynamicTimeNoSSR }
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Habit } from '@/lib/types'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Coins, Edit, Trash2, Check, Undo2 } from 'lucide-react'
|
||||
@@ -13,7 +15,8 @@ interface HabitItemProps {
|
||||
}
|
||||
|
||||
export default function HabitItem({ habit, onEdit, onDelete, onComplete, onUndo }: HabitItemProps) {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const { settings } = useSettings()
|
||||
const today = getTodayInTimezone(settings.system.timezone)
|
||||
const isCompletedToday = habit.completions?.includes(today)
|
||||
const [isHighlighted, setIsHighlighted] = useState(false)
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { BarChart } from 'lucide-react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
import { loadHabitsData } from '@/app/actions/data'
|
||||
import { Habit } from '@/lib/types'
|
||||
|
||||
@@ -15,7 +17,8 @@ export default function HabitOverview() {
|
||||
fetchHabits()
|
||||
}, [])
|
||||
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const { settings } = useSettings()
|
||||
const today = getTodayInTimezone(settings.system.timezone)
|
||||
|
||||
const completedToday = habits.filter(habit =>
|
||||
habit.completions.includes(today)
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
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 { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
|
||||
interface HabitStreakProps {
|
||||
@@ -11,7 +13,8 @@ interface HabitStreakProps {
|
||||
export default function HabitStreak({ habits }: HabitStreakProps) {
|
||||
// Get the last 30 days of data
|
||||
const dates = Array.from({ length: 30 }, (_, i) => {
|
||||
const d = new Date()
|
||||
const { settings } = useSettings()
|
||||
const d = getDateInTimezone(new Date(), settings.system.timezone)
|
||||
d.setDate(d.getDate() - i)
|
||||
return d.toISOString().split('T')[0]
|
||||
}).reverse()
|
||||
|
||||
7
docker-compose.yaml
Normal file
7
docker-compose.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
services:
|
||||
habittrove:
|
||||
ports:
|
||||
- "3000:3000"
|
||||
volumes:
|
||||
- "./data:/app/data" # Use a relative path instead of $(pwd)
|
||||
image: dohsimpson/habittrove
|
||||
@@ -1,12 +1,15 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useSettings } from '@/hooks/useSettings'
|
||||
import { loadHabitsData, saveHabitsData, addCoins, removeCoins } from '@/app/actions/data'
|
||||
import { toast } from '@/hooks/use-toast'
|
||||
import { ToastAction } from '@/components/ui/toast'
|
||||
import { Undo2 } from 'lucide-react'
|
||||
import { Habit } from '@/lib/types'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
|
||||
export function useHabits() {
|
||||
const [habits, setHabits] = useState<Habit[]>([])
|
||||
const { settings } = useSettings()
|
||||
|
||||
useEffect(() => {
|
||||
fetchHabits()
|
||||
@@ -39,7 +42,7 @@ export function useHabits() {
|
||||
}
|
||||
|
||||
const completeHabit = async (habit: Habit) => {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const today = getTodayInTimezone(settings.system.timezone)
|
||||
if (!habit.completions.includes(today)) {
|
||||
const updatedHabit = {
|
||||
...habit,
|
||||
|
||||
@@ -58,6 +58,9 @@ export const getDefaultSettings = (): Settings => ({
|
||||
ui: {
|
||||
useNumberFormatting: true,
|
||||
useGrouping: true,
|
||||
},
|
||||
system: {
|
||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
}
|
||||
});
|
||||
|
||||
@@ -77,6 +80,11 @@ export interface UISettings {
|
||||
useGrouping: boolean;
|
||||
}
|
||||
|
||||
export interface SystemSettings {
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
ui: UISettings;
|
||||
system: SystemSettings;
|
||||
}
|
||||
|
||||
50
lib/utils.test.ts
Normal file
50
lib/utils.test.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { expect, test, describe, beforeAll, afterAll } from "bun:test";
|
||||
import { cn, getDateInTimezone, getTodayInTimezone } from './utils'
|
||||
|
||||
describe('cn utility', () => {
|
||||
test('should merge class names correctly', () => {
|
||||
expect(cn('foo', 'bar')).toBe('foo bar')
|
||||
expect(cn('foo', { bar: true })).toBe('foo bar')
|
||||
expect(cn('foo', { bar: false })).toBe('foo')
|
||||
expect(cn('foo', ['bar', 'baz'])).toBe('foo bar baz')
|
||||
})
|
||||
})
|
||||
|
||||
describe('timezone utilities', () => {
|
||||
describe('getDateInTimezone', () => {
|
||||
test('should convert date to specified timezone', () => {
|
||||
const date = new Date('2024-01-01T00:00:00Z')
|
||||
|
||||
// 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')
|
||||
})
|
||||
})
|
||||
|
||||
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')
|
||||
})
|
||||
})
|
||||
})
|
||||
10
lib/utils.ts
10
lib/utils.ts
@@ -1,6 +1,16 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
import moment from "moment-timezone"
|
||||
|
||||
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]'));
|
||||
}
|
||||
|
||||
export function getTodayInTimezone(timezone: string): string {
|
||||
return moment.tz(timezone).format('YYYY-MM-DD');
|
||||
}
|
||||
|
||||
68
package-lock.json
generated
68
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "habittrove",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "habittrove",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"dependencies": {
|
||||
"@next/font": "^14.2.15",
|
||||
"@radix-ui/react-dialog": "^1.1.4",
|
||||
@@ -16,6 +16,7 @@
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@uiw/react-heat-map": "^2.3.2",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
@@ -24,6 +25,8 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"js-confetti": "^0.12.0",
|
||||
"lucide-react": "^0.469.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.46",
|
||||
"next": "15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-confetti": "^6.2.2",
|
||||
@@ -1566,6 +1569,14 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/bun": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.1.14.tgz",
|
||||
"integrity": "sha512-opVYiFGtO2af0dnWBdZWlioLBoxSdDO5qokaazLhq8XQtGZbY4pY3/JxY8Zdf/hEwGubbp7ErZXoN1+h2yesxA==",
|
||||
"dependencies": {
|
||||
"bun-types": "1.1.37"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/canvas-confetti": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz",
|
||||
@@ -1705,7 +1716,6 @@
|
||||
"version": "20.17.10",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz",
|
||||
"integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.19.2"
|
||||
}
|
||||
@@ -1732,6 +1742,14 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.5.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz",
|
||||
"integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.18.2",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz",
|
||||
@@ -2593,6 +2611,28 @@
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/bun-types": {
|
||||
"version": "1.1.37",
|
||||
"resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.1.37.tgz",
|
||||
"integrity": "sha512-C65lv6eBr3LPJWFZ2gswyrGZ82ljnH8flVE03xeXxKhi2ZGtFiO4isRKTKnitbSqtRAcaqYSR6djt1whI66AbA==",
|
||||
"dependencies": {
|
||||
"@types/node": "~20.12.8",
|
||||
"@types/ws": "~8.5.10"
|
||||
}
|
||||
},
|
||||
"node_modules/bun-types/node_modules/@types/node": {
|
||||
"version": "20.12.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.14.tgz",
|
||||
"integrity": "sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/bun-types/node_modules/undici-types": {
|
||||
"version": "5.26.5",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -5913,6 +5953,25 @@
|
||||
"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",
|
||||
@@ -7997,8 +8056,7 @@
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.19.8",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||
},
|
||||
"node_modules/unified": {
|
||||
"version": "11.0.5",
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"@radix-ui/react-switch": "^1.1.2",
|
||||
"@radix-ui/react-toast": "^1.2.4",
|
||||
"@types/bun": "^1.1.14",
|
||||
"@types/canvas-confetti": "^1.9.0",
|
||||
"@uiw/react-heat-map": "^2.3.2",
|
||||
"canvas-confetti": "^1.9.3",
|
||||
@@ -27,6 +28,8 @@
|
||||
"date-fns": "^3.6.0",
|
||||
"js-confetti": "^0.12.0",
|
||||
"lucide-react": "^0.469.0",
|
||||
"moment": "^2.30.1",
|
||||
"moment-timezone": "^0.5.46",
|
||||
"next": "15.1.3",
|
||||
"react": "^19.0.0",
|
||||
"react-confetti": "^6.2.2",
|
||||
|
||||
Reference in New Issue
Block a user