mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
added timezone settings
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Version 0.1.5
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- docker-compose.yaml
|
||||||
|
- timezone settings
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- completing habits now respect timezone settings
|
||||||
|
|
||||||
## Version 0.1.4
|
## Version 0.1.4
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|||||||
@@ -124,6 +124,9 @@ export async function loadSettings(): Promise<Settings> {
|
|||||||
ui: {
|
ui: {
|
||||||
useNumberFormatting: true,
|
useNumberFormatting: true,
|
||||||
useGrouping: 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 { Switch } from '@/components/ui/switch'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
|
import { DynamicTimeNoSSR } from '@/components/DynamicTimeNoSSR'
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { settings, updateSettings } = useSettings()
|
const { settings, updateSettings } = useSettings()
|
||||||
@@ -13,7 +14,6 @@ export default function SettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="container mx-auto px-4 py-8">
|
<div className="container mx-auto px-4 py-8">
|
||||||
<h1 className="text-3xl font-bold mb-6">Settings</h1>
|
<h1 className="text-3xl font-bold mb-6">Settings</h1>
|
||||||
|
|
||||||
<Card className="mb-6">
|
<Card className="mb-6">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>UI Settings</CardTitle>
|
<CardTitle>UI Settings</CardTitle>
|
||||||
@@ -58,6 +58,42 @@ export default function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useSettings } from '@/hooks/useSettings'
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
|
import { getDateInTimezone } from '@/lib/utils'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { formatNumber } from '@/lib/utils/formatNumber'
|
import { formatNumber } from '@/lib/utils/formatNumber'
|
||||||
import { History } from 'lucide-react'
|
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 { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { useCoins } from '@/hooks/useCoins'
|
import { useCoins } from '@/hooks/useCoins'
|
||||||
import { format } from 'date-fns'
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
export default function CoinsManager() {
|
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-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">
|
<div className="text-2xl font-bold text-blue-900 dark:text-blue-50">
|
||||||
{transactions.filter(t =>
|
{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} 📊
|
).length} 📊
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -212,7 +213,7 @@ export default function CoinsManager() {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500">
|
||||||
{format(new Date(transaction.timestamp), 'PPpp')}
|
{getDateInTimezone(transaction.timestamp, settings.system.timezone).toLocaleString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp } from 'lucide-react'
|
import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp } from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
|
import { getTodayInTimezone } from '@/lib/utils'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
@@ -22,7 +24,8 @@ export default function DailyOverview({
|
|||||||
onComplete,
|
onComplete,
|
||||||
onUndo
|
onUndo
|
||||||
}: UpcomingItemsProps) {
|
}: UpcomingItemsProps) {
|
||||||
const today = new Date().toISOString().split('T')[0]
|
const { settings } = useSettings()
|
||||||
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
const todayCompletions = habits.filter(habit =>
|
const todayCompletions = habits.filter(habit =>
|
||||||
habit.completions.includes(today)
|
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 { 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 { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Coins, Edit, Trash2, Check, Undo2 } from 'lucide-react'
|
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) {
|
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 isCompletedToday = habit.completions?.includes(today)
|
||||||
const [isHighlighted, setIsHighlighted] = useState(false)
|
const [isHighlighted, setIsHighlighted] = useState(false)
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { BarChart } from 'lucide-react'
|
import { BarChart } from 'lucide-react'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
|
import { getTodayInTimezone } from '@/lib/utils'
|
||||||
import { loadHabitsData } from '@/app/actions/data'
|
import { loadHabitsData } from '@/app/actions/data'
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
|
|
||||||
@@ -15,7 +17,8 @@ export default function HabitOverview() {
|
|||||||
fetchHabits()
|
fetchHabits()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const today = new Date().toISOString().split('T')[0]
|
const { settings } = useSettings()
|
||||||
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
|
|
||||||
const completedToday = habits.filter(habit =>
|
const completedToday = habits.filter(habit =>
|
||||||
habit.completions.includes(today)
|
habit.completions.includes(today)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
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'
|
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||||
|
|
||||||
interface HabitStreakProps {
|
interface HabitStreakProps {
|
||||||
@@ -11,7 +13,8 @@ interface HabitStreakProps {
|
|||||||
export default function HabitStreak({ habits }: HabitStreakProps) {
|
export default function HabitStreak({ habits }: HabitStreakProps) {
|
||||||
// Get the last 30 days of data
|
// Get the last 30 days of data
|
||||||
const dates = Array.from({ length: 30 }, (_, i) => {
|
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)
|
d.setDate(d.getDate() - i)
|
||||||
return d.toISOString().split('T')[0]
|
return d.toISOString().split('T')[0]
|
||||||
}).reverse()
|
}).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 { useState, useEffect } from 'react'
|
||||||
|
import { useSettings } from '@/hooks/useSettings'
|
||||||
import { loadHabitsData, saveHabitsData, addCoins, removeCoins } from '@/app/actions/data'
|
import { loadHabitsData, saveHabitsData, addCoins, removeCoins } from '@/app/actions/data'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { ToastAction } from '@/components/ui/toast'
|
import { ToastAction } from '@/components/ui/toast'
|
||||||
import { Undo2 } from 'lucide-react'
|
import { Undo2 } from 'lucide-react'
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
|
import { getTodayInTimezone } from '@/lib/utils'
|
||||||
|
|
||||||
export function useHabits() {
|
export function useHabits() {
|
||||||
const [habits, setHabits] = useState<Habit[]>([])
|
const [habits, setHabits] = useState<Habit[]>([])
|
||||||
|
const { settings } = useSettings()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchHabits()
|
fetchHabits()
|
||||||
@@ -39,7 +42,7 @@ export function useHabits() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const completeHabit = async (habit: Habit) => {
|
const completeHabit = async (habit: Habit) => {
|
||||||
const today = new Date().toISOString().split('T')[0]
|
const today = getTodayInTimezone(settings.system.timezone)
|
||||||
if (!habit.completions.includes(today)) {
|
if (!habit.completions.includes(today)) {
|
||||||
const updatedHabit = {
|
const updatedHabit = {
|
||||||
...habit,
|
...habit,
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ export const getDefaultSettings = (): Settings => ({
|
|||||||
ui: {
|
ui: {
|
||||||
useNumberFormatting: true,
|
useNumberFormatting: true,
|
||||||
useGrouping: true,
|
useGrouping: true,
|
||||||
|
},
|
||||||
|
system: {
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -77,6 +80,11 @@ export interface UISettings {
|
|||||||
useGrouping: boolean;
|
useGrouping: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SystemSettings {
|
||||||
|
timezone: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
ui: UISettings;
|
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 { clsx, type ClassValue } from "clsx"
|
||||||
import { twMerge } from "tailwind-merge"
|
import { twMerge } from "tailwind-merge"
|
||||||
|
import moment from "moment-timezone"
|
||||||
|
|
||||||
export function cn(...inputs: ClassValue[]) {
|
export function cn(...inputs: ClassValue[]) {
|
||||||
return twMerge(clsx(inputs))
|
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",
|
"name": "habittrove",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "habittrove",
|
"name": "habittrove",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/font": "^14.2.15",
|
"@next/font": "^14.2.15",
|
||||||
"@radix-ui/react-dialog": "^1.1.4",
|
"@radix-ui/react-dialog": "^1.1.4",
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.4",
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
|
"@types/bun": "^1.1.14",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@uiw/react-heat-map": "^2.3.2",
|
"@uiw/react-heat-map": "^2.3.2",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
@@ -24,6 +25,8 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"js-confetti": "^0.12.0",
|
"js-confetti": "^0.12.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"moment-timezone": "^0.5.46",
|
||||||
"next": "15.1.3",
|
"next": "15.1.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-confetti": "^6.2.2",
|
"react-confetti": "^6.2.2",
|
||||||
@@ -1566,6 +1569,14 @@
|
|||||||
"node": ">=4"
|
"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": {
|
"node_modules/@types/canvas-confetti": {
|
||||||
"version": "1.9.0",
|
"version": "1.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.9.0.tgz",
|
||||||
@@ -1705,7 +1716,6 @@
|
|||||||
"version": "20.17.10",
|
"version": "20.17.10",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.10.tgz",
|
||||||
"integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==",
|
"integrity": "sha512-/jrvh5h6NXhEauFFexRin69nA0uHJ5gwk4iDivp/DeoEua3uwCUto6PC86IpRITBOs4+6i2I56K5x5b6WYGXHA==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
@@ -1732,6 +1742,14 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
|
||||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="
|
"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": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.18.2",
|
"version": "8.18.2",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.2.tgz",
|
||||||
@@ -2593,6 +2611,28 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"peer": 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": {
|
"node_modules/busboy": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||||
@@ -5913,6 +5953,25 @@
|
|||||||
"node": ">=16 || 14 >=14.17"
|
"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": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
@@ -7997,8 +8056,7 @@
|
|||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/unified": {
|
"node_modules/unified": {
|
||||||
"version": "11.0.5",
|
"version": "11.0.5",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
"@radix-ui/react-slot": "^1.1.1",
|
"@radix-ui/react-slot": "^1.1.1",
|
||||||
"@radix-ui/react-switch": "^1.1.2",
|
"@radix-ui/react-switch": "^1.1.2",
|
||||||
"@radix-ui/react-toast": "^1.2.4",
|
"@radix-ui/react-toast": "^1.2.4",
|
||||||
|
"@types/bun": "^1.1.14",
|
||||||
"@types/canvas-confetti": "^1.9.0",
|
"@types/canvas-confetti": "^1.9.0",
|
||||||
"@uiw/react-heat-map": "^2.3.2",
|
"@uiw/react-heat-map": "^2.3.2",
|
||||||
"canvas-confetti": "^1.9.3",
|
"canvas-confetti": "^1.9.3",
|
||||||
@@ -27,6 +28,8 @@
|
|||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"js-confetti": "^0.12.0",
|
"js-confetti": "^0.12.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"moment-timezone": "^0.5.46",
|
||||||
"next": "15.1.3",
|
"next": "15.1.3",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-confetti": "^6.2.2",
|
"react-confetti": "^6.2.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user