mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-03-08 03:29:49 +01:00
Compare commits
3 Commits
c397f40239
...
630363af1f
| Author | SHA1 | Date | |
|---|---|---|---|
|
630363af1f
|
|||
|
f7034116a3
|
|||
|
c418bddd9e
|
@@ -48,23 +48,6 @@ export default async function RootLayout({
|
|||||||
// set suppressHydrationWarning to true to prevent hydration errors when using ThemeProvider (https://ui.shadcn.com/docs/dark-mode/next)
|
// set suppressHydrationWarning to true to prevent hydration errors when using ThemeProvider (https://ui.shadcn.com/docs/dark-mode/next)
|
||||||
<html lang={locale} suppressHydrationWarning>
|
<html lang={locale} suppressHydrationWarning>
|
||||||
<body className={activeFont.className}>
|
<body className={activeFont.className}>
|
||||||
<script
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `
|
|
||||||
if ('serviceWorker' in navigator) {
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
navigator.serviceWorker.register('/sw.js')
|
|
||||||
.then(registration => {
|
|
||||||
console.log('ServiceWorker registration successful');
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.log('ServiceWorker registration failed: ', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<JotaiProvider>
|
<JotaiProvider>
|
||||||
<Suspense fallback={<LoadingSpinner />}>
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
<JotaiHydrate
|
<JotaiHydrate
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useCoins } from '@/hooks/useCoins'
|
|||||||
import { currentUserAtom, settingsAtom, usersAtom } from '@/lib/atoms'
|
import { currentUserAtom, settingsAtom, usersAtom } from '@/lib/atoms'
|
||||||
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
import { MAX_COIN_LIMIT } from '@/lib/constants'
|
||||||
import { TransactionType } from '@/lib/types'
|
import { TransactionType } from '@/lib/types'
|
||||||
import { d2s, t2d } from '@/lib/utils'
|
import { calculateTransactionsToday, d2s, t2d } from '@/lib/utils'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { History } from 'lucide-react'
|
import { History } from 'lucide-react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
@@ -33,8 +33,7 @@ export default function CoinsManager() {
|
|||||||
coinsEarnedToday,
|
coinsEarnedToday,
|
||||||
totalEarned,
|
totalEarned,
|
||||||
totalSpent,
|
totalSpent,
|
||||||
coinsSpentToday,
|
coinsSpentToday
|
||||||
transactionsToday
|
|
||||||
} = useCoins({ selectedUser })
|
} = useCoins({ selectedUser })
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const [usersData] = useAtom(usersAtom)
|
const [usersData] = useAtom(usersAtom)
|
||||||
@@ -252,7 +251,7 @@ export default function CoinsManager() {
|
|||||||
<div className="p-4 rounded-lg bg-orange-100 dark:bg-orange-900">
|
<div className="p-4 rounded-lg bg-orange-100 dark:bg-orange-900">
|
||||||
<div className="text-sm text-orange-800 dark:text-orange-100 mb-1">{t('todaysTransactionsLabel')}</div>
|
<div className="text-sm text-orange-800 dark:text-orange-100 mb-1">{t('todaysTransactionsLabel')}</div>
|
||||||
<div className="text-2xl font-bold text-orange-900 dark:text-orange-50">
|
<div className="text-2xl font-bold text-orange-900 dark:text-orange-50">
|
||||||
{transactionsToday} 📊
|
{calculateTransactionsToday(transactions, settings.system.timezone)} 📊
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,22 +13,22 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip"
|
} from "@/components/ui/tooltip"
|
||||||
import { useHabits } from '@/hooks/useHabits'
|
import { useHabits } from '@/hooks/useHabits'
|
||||||
import { browserSettingsAtom, completedHabitsMapAtom, hasTasksAtom, pomodoroAtom, settingsAtom } from '@/lib/atoms'
|
import { browserSettingsAtom, completedHabitsMapAtom, settingsAtom } from '@/lib/atoms'
|
||||||
import { DESKTOP_DISPLAY_ITEM_COUNT } from '@/lib/constants'
|
import { DESKTOP_DISPLAY_ITEM_COUNT } from '@/lib/constants'
|
||||||
import { Habit, WishlistItemType } from '@/lib/types'
|
import { Habit, WishlistItemType } from '@/lib/types'
|
||||||
import { cn, d2t, getNow, getTodayInTimezone, isHabitDue, isSameDate, isTaskOverdue, t2d } from '@/lib/utils'
|
import { cn, d2t, getNow, getTodayInTimezone, isHabitDue, isSameDate, isTaskOverdue, t2d } from '@/lib/utils'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { AlertTriangle, ArrowRight, ChevronDown, ChevronUp, Circle, CircleCheck, Coins, Pin, Plus } from 'lucide-react'; // Removed unused icons
|
import { AlertTriangle, ArrowRight, ChevronDown, ChevronUp, Circle, CircleCheck, Coins, Pin, Plus } from 'lucide-react';
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import AddEditHabitModal from './AddEditHabitModal'
|
import AddEditHabitModal from './AddEditHabitModal'
|
||||||
import CompletionCountBadge from './CompletionCountBadge'
|
import CompletionCountBadge from './CompletionCountBadge'
|
||||||
import ConfirmDialog from './ConfirmDialog'
|
import ConfirmDialog from './ConfirmDialog'
|
||||||
|
import DrawingDisplay from './DrawingDisplay'
|
||||||
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
import { HabitContextMenuItems } from './HabitContextMenuItems'
|
||||||
import Linkify from './linkify'
|
import Linkify from './linkify'
|
||||||
import { Button } from './ui/button'
|
import { Button } from './ui/button'
|
||||||
import DrawingDisplay from './DrawingDisplay'
|
|
||||||
|
|
||||||
interface UpcomingItemsProps {
|
interface UpcomingItemsProps {
|
||||||
habits: Habit[]
|
habits: Habit[]
|
||||||
@@ -54,8 +54,7 @@ const ItemSection = ({
|
|||||||
addNewItem,
|
addNewItem,
|
||||||
}: ItemSectionProps) => {
|
}: ItemSectionProps) => {
|
||||||
const t = useTranslations('DailyOverview');
|
const t = useTranslations('DailyOverview');
|
||||||
const { completeHabit, undoComplete, saveHabit, deleteHabit, archiveHabit, habitFreqMap } = useHabits();
|
const { completeHabit, undoComplete, saveHabit, deleteHabit, habitFreqMap } = useHabits();
|
||||||
const [_, setPomo] = useAtom(pomodoroAtom);
|
|
||||||
const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom);
|
const [browserSettings, setBrowserSettings] = useAtom(browserSettingsAtom);
|
||||||
const [settings] = useAtom(settingsAtom);
|
const [settings] = useAtom(settingsAtom);
|
||||||
const [completedHabitsMap] = useAtom(completedHabitsMapAtom);
|
const [completedHabitsMap] = useAtom(completedHabitsMapAtom);
|
||||||
@@ -397,8 +396,6 @@ export default function DailyOverview({
|
|||||||
return a.coinCost - b.coinCost
|
return a.coinCost - b.coinCost
|
||||||
})
|
})
|
||||||
|
|
||||||
const [hasTasks] = useAtom(hasTasksAtom)
|
|
||||||
const [, setPomo] = useAtom(pomodoroAtom)
|
|
||||||
const [modalConfig, setModalConfig] = useState<{
|
const [modalConfig, setModalConfig] = useState<{
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
isTask: boolean
|
isTask: boolean
|
||||||
@@ -416,7 +413,7 @@ export default function DailyOverview({
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Tasks Section */}
|
{/* Tasks Section */}
|
||||||
{hasTasks && (
|
{habits.some(habit => habit.isTask === true) && (
|
||||||
<ItemSection
|
<ItemSection
|
||||||
title={t('dailyTasksTitle')}
|
title={t('dailyTasksTitle')}
|
||||||
items={dailyTasks}
|
items={dailyTasks}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import CompletionCountBadge from '@/components/CompletionCountBadge'
|
|||||||
import { Calendar } from '@/components/ui/calendar'
|
import { Calendar } from '@/components/ui/calendar'
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
import { useHabits } from '@/hooks/useHabits'
|
import { useHabits } from '@/hooks/useHabits'
|
||||||
import { completedHabitsMapAtom, habitsAtom, hasTasksAtom, settingsAtom } from '@/lib/atoms'
|
import { completedHabitsMapAtom, habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||||
import { Habit } from '@/lib/types'
|
import { Habit } from '@/lib/types'
|
||||||
import { d2s, getCompletionsForDate, getISODate, getNow, isHabitDue } from '@/lib/utils'
|
import { d2s, getCompletionsForDate, getISODate, getNow, isHabitDue } from '@/lib/utils'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
@@ -29,7 +29,6 @@ export default function HabitCalendar() {
|
|||||||
const [selectedDateTime, setSelectedDateTime] = useState<DateTime>(getNow({ timezone: settings.system.timezone }))
|
const [selectedDateTime, setSelectedDateTime] = useState<DateTime>(getNow({ timezone: settings.system.timezone }))
|
||||||
const selectedDate = selectedDateTime.toFormat("yyyy-MM-dd")
|
const selectedDate = selectedDateTime.toFormat("yyyy-MM-dd")
|
||||||
const [habitsData] = useAtom(habitsAtom)
|
const [habitsData] = useAtom(habitsAtom)
|
||||||
const [hasTasks] = useAtom(hasTasksAtom)
|
|
||||||
const habits = habitsData.habits
|
const habits = habitsData.habits
|
||||||
|
|
||||||
const [completedHabitsMap] = useAtom(completedHabitsMapAtom)
|
const [completedHabitsMap] = useAtom(completedHabitsMapAtom)
|
||||||
@@ -83,7 +82,7 @@ export default function HabitCalendar() {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
{selectedDateTime && (
|
{selectedDateTime && (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
{hasTasks && (
|
{habits.some(habit => habit.isTask === true) && (
|
||||||
<div className="pt-2 border-t">
|
<div className="pt-2 border-t">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">{t('tasksSectionTitle')}</h3>
|
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">{t('tasksSectionTitle')}</h3>
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { completedHabitsMapAtom, hasTasksAtom, settingsAtom } from '@/lib/atoms'; // Added completedHabitsMapAtom
|
import { completedHabitsMapAtom, settingsAtom } from '@/lib/atoms';
|
||||||
import { Habit } from '@/lib/types';
|
import { Habit } from '@/lib/types';
|
||||||
import { d2s, getNow } from '@/lib/utils'; // Removed getCompletedHabitsForDate
|
import { d2s, getNow } from '@/lib/utils';
|
||||||
import { useAtom } from 'jotai';
|
import { useAtom } from 'jotai';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
import { CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
|
||||||
|
export default function HabitStreak({ habits }: { habits: Habit[] }) {
|
||||||
interface HabitStreakProps {
|
|
||||||
habits: Habit[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HabitStreak({ habits }: HabitStreakProps) {
|
|
||||||
const t = useTranslations('HabitStreak');
|
const t = useTranslations('HabitStreak');
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const [hasTasks] = useAtom(hasTasksAtom)
|
|
||||||
const [completedHabitsMap] = useAtom(completedHabitsMapAtom) // Use the atom
|
const [completedHabitsMap] = useAtom(completedHabitsMapAtom) // Use the atom
|
||||||
|
|
||||||
// Get the last 7 days of data
|
// Get the last 7 days of data
|
||||||
@@ -72,7 +66,7 @@ export default function HabitStreak({ habits }: HabitStreakProps) {
|
|||||||
strokeWidth={2}
|
strokeWidth={2}
|
||||||
dot={false}
|
dot={false}
|
||||||
/>
|
/>
|
||||||
{hasTasks && (
|
{habits.some(habit => habit.isTask === true) && (
|
||||||
<Line
|
<Line
|
||||||
type="monotone"
|
type="monotone"
|
||||||
name={t('tooltipTasksLabel')}
|
name={t('tooltipTasksLabel')}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
import { useHabits } from '@/hooks/useHabits'
|
import { useHabits } from '@/hooks/useHabits'
|
||||||
import { habitsAtom, pomodoroAtom, pomodoroTodayCompletionsAtom } from '@/lib/atoms'
|
import { habitsAtom, pomodoroAtom, settingsAtom } from '@/lib/atoms'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn, getTodayCompletions } from '@/lib/utils'
|
||||||
import { useAtom } from 'jotai'
|
import { useAtom } from 'jotai'
|
||||||
import { Clock, Minus, Pause, Play, RotateCw, SkipForward, X } from 'lucide-react'
|
import { Clock, Minus, Pause, Play, RotateCw, SkipForward, X } from 'lucide-react'
|
||||||
import { useTranslations } from 'next-intl'
|
import { useTranslations } from 'next-intl'
|
||||||
@@ -41,12 +41,13 @@ export default function PomodoroTimer() {
|
|||||||
const [pomo, setPomo] = useAtom(pomodoroAtom)
|
const [pomo, setPomo] = useAtom(pomodoroAtom)
|
||||||
const { show, selectedHabitId, autoStart, minimized } = pomo
|
const { show, selectedHabitId, autoStart, minimized } = pomo
|
||||||
const [habitsData] = useAtom(habitsAtom)
|
const [habitsData] = useAtom(habitsAtom)
|
||||||
|
const [settingsData] = useAtom(settingsAtom)
|
||||||
const { completeHabit } = useHabits()
|
const { completeHabit } = useHabits()
|
||||||
const selectedHabit = selectedHabitId ? habitsData.habits.find(habit => habit.id === selectedHabitId) : null
|
const selectedHabit = selectedHabitId ? habitsData.habits.find(habit => habit.id === selectedHabitId) : null
|
||||||
const [timeLeft, setTimeLeft] = useState(PomoConfigs.focus.duration)
|
const [timeLeft, setTimeLeft] = useState(PomoConfigs.focus.duration)
|
||||||
const [state, setState] = useState<'started' | 'stopped' | 'paused'>(autoStart ? 'started' : 'stopped')
|
const [state, setState] = useState<'started' | 'stopped' | 'paused'>(autoStart ? 'started' : 'stopped')
|
||||||
const wakeLock = useRef<WakeLockSentinel | null>(null)
|
const wakeLock = useRef<WakeLockSentinel | null>(null)
|
||||||
const [todayCompletions] = useAtom(pomodoroTodayCompletionsAtom)
|
const todayCompletions = getTodayCompletions(pomo, habitsData, settingsData);
|
||||||
const currentTimerRef = useRef<PomoConfig>(PomoConfigs.focus)
|
const currentTimerRef = useRef<PomoConfig>(PomoConfigs.focus)
|
||||||
const [currentLabel, setCurrentLabel] = useState(() => {
|
const [currentLabel, setCurrentLabel] = useState(() => {
|
||||||
const labels = currentTimerRef.current.getLabels();
|
const labels = currentTimerRef.current.getLabels();
|
||||||
|
|||||||
@@ -2,15 +2,14 @@ import { addCoins, removeCoins, saveCoinsData } from '@/app/actions/data';
|
|||||||
import { toast } from '@/hooks/use-toast';
|
import { toast } from '@/hooks/use-toast';
|
||||||
import {
|
import {
|
||||||
coinsAtom,
|
coinsAtom,
|
||||||
coinsBalanceAtom,
|
|
||||||
coinsEarnedTodayAtom,
|
coinsEarnedTodayAtom,
|
||||||
coinsSpentTodayAtom,
|
coinsSpentTodayAtom,
|
||||||
currentUserAtom,
|
currentUserAtom,
|
||||||
|
currentUserIdAtom,
|
||||||
settingsAtom,
|
settingsAtom,
|
||||||
totalEarnedAtom,
|
totalEarnedAtom,
|
||||||
totalSpentAtom,
|
totalSpentAtom,
|
||||||
transactionsTodayAtom,
|
usersAtom
|
||||||
usersAtom,
|
|
||||||
} from '@/lib/atoms';
|
} from '@/lib/atoms';
|
||||||
import { MAX_COIN_LIMIT } from '@/lib/constants';
|
import { MAX_COIN_LIMIT } from '@/lib/constants';
|
||||||
import { CoinsData } from '@/lib/types';
|
import { CoinsData } from '@/lib/types';
|
||||||
@@ -24,27 +23,26 @@ export function useCoins(options?: { selectedUser?: string }) {
|
|||||||
const tCommon = useTranslations('Common');
|
const tCommon = useTranslations('Common');
|
||||||
const [coins, setCoins] = useAtom(coinsAtom)
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const [users] = useAtom(usersAtom)
|
const [{users}] = useAtom(usersAtom)
|
||||||
const [currentUser] = useAtom(currentUserAtom)
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
const [allCoinsData] = useAtom(coinsAtom) // All coin transactions
|
const [coinsData] = useAtom(coinsAtom) // All coin transactions
|
||||||
const [loggedInUserBalance] = useAtom(coinsBalanceAtom) // Balance of the *currently logged-in* user
|
const [loggedInUserId] = useAtom(currentUserIdAtom);
|
||||||
|
const loggedInUserBalance = loggedInUserId ? coins.transactions.filter(transaction => transaction.userId === loggedInUserId).reduce((sum, transaction) => sum + transaction.amount, 0) : 0;
|
||||||
const [atomCoinsEarnedToday] = useAtom(coinsEarnedTodayAtom);
|
const [atomCoinsEarnedToday] = useAtom(coinsEarnedTodayAtom);
|
||||||
const [atomTotalEarned] = useAtom(totalEarnedAtom)
|
const [atomTotalEarned] = useAtom(totalEarnedAtom)
|
||||||
const [atomTotalSpent] = useAtom(totalSpentAtom)
|
const [atomTotalSpent] = useAtom(totalSpentAtom)
|
||||||
const [atomCoinsSpentToday] = useAtom(coinsSpentTodayAtom);
|
const [atomCoinsSpentToday] = useAtom(coinsSpentTodayAtom);
|
||||||
const [atomTransactionsToday] = useAtom(transactionsTodayAtom);
|
const targetUser = options?.selectedUser ? users.find(u => u.id === options.selectedUser) : currentUser
|
||||||
const targetUser = options?.selectedUser ? users.users.find(u => u.id === options.selectedUser) : currentUser
|
|
||||||
|
|
||||||
const transactions = useMemo(() => {
|
const transactions = useMemo(() => {
|
||||||
return allCoinsData.transactions.filter(t => t.userId === targetUser?.id);
|
return coinsData.transactions.filter(t => t.userId === targetUser?.id);
|
||||||
}, [allCoinsData, targetUser?.id]);
|
}, [coinsData, targetUser?.id]);
|
||||||
|
|
||||||
const timezone = settings.system.timezone;
|
const timezone = settings.system.timezone;
|
||||||
const [coinsEarnedToday, setCoinsEarnedToday] = useState(0);
|
const [coinsEarnedToday, setCoinsEarnedToday] = useState(0);
|
||||||
const [totalEarned, setTotalEarned] = useState(0);
|
const [totalEarned, setTotalEarned] = useState(0);
|
||||||
const [totalSpent, setTotalSpent] = useState(0);
|
const [totalSpent, setTotalSpent] = useState(0);
|
||||||
const [coinsSpentToday, setCoinsSpentToday] = useState(0);
|
const [coinsSpentToday, setCoinsSpentToday] = useState(0);
|
||||||
const [transactionsToday, setTransactionsToday] = useState<number>(0);
|
|
||||||
const [balance, setBalance] = useState(0);
|
const [balance, setBalance] = useState(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -55,7 +53,6 @@ export function useCoins(options?: { selectedUser?: string }) {
|
|||||||
setTotalEarned(atomTotalEarned);
|
setTotalEarned(atomTotalEarned);
|
||||||
setTotalSpent(atomTotalSpent);
|
setTotalSpent(atomTotalSpent);
|
||||||
setCoinsSpentToday(atomCoinsSpentToday);
|
setCoinsSpentToday(atomCoinsSpentToday);
|
||||||
setTransactionsToday(atomTransactionsToday);
|
|
||||||
setBalance(loggedInUserBalance);
|
setBalance(loggedInUserBalance);
|
||||||
} else if (targetUser?.id) {
|
} else if (targetUser?.id) {
|
||||||
// If an admin is viewing another user, calculate their metrics manually
|
// If an admin is viewing another user, calculate their metrics manually
|
||||||
@@ -71,8 +68,6 @@ export function useCoins(options?: { selectedUser?: string }) {
|
|||||||
const spentToday = calculateCoinsSpentToday(transactions, timezone);
|
const spentToday = calculateCoinsSpentToday(transactions, timezone);
|
||||||
setCoinsSpentToday(roundToInteger(spentToday));
|
setCoinsSpentToday(roundToInteger(spentToday));
|
||||||
|
|
||||||
setTransactionsToday(calculateTransactionsToday(transactions, timezone)); // This is a count
|
|
||||||
|
|
||||||
const calculatedBalance = transactions.reduce((acc, t) => acc + t.amount, 0);
|
const calculatedBalance = transactions.reduce((acc, t) => acc + t.amount, 0);
|
||||||
setBalance(roundToInteger(calculatedBalance));
|
setBalance(roundToInteger(calculatedBalance));
|
||||||
}
|
}
|
||||||
@@ -85,8 +80,7 @@ export function useCoins(options?: { selectedUser?: string }) {
|
|||||||
atomCoinsEarnedToday,
|
atomCoinsEarnedToday,
|
||||||
atomTotalEarned,
|
atomTotalEarned,
|
||||||
atomTotalSpent,
|
atomTotalSpent,
|
||||||
atomCoinsSpentToday,
|
atomCoinsSpentToday
|
||||||
atomTransactionsToday,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const add = async (amount: number, description: string, note?: string) => {
|
const add = async (amount: number, description: string, note?: string) => {
|
||||||
@@ -187,7 +181,6 @@ export function useCoins(options?: { selectedUser?: string }) {
|
|||||||
coinsEarnedToday,
|
coinsEarnedToday,
|
||||||
totalEarned,
|
totalEarned,
|
||||||
totalSpent,
|
totalSpent,
|
||||||
coinsSpentToday,
|
coinsSpentToday
|
||||||
transactionsToday
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { addCoins, removeCoins, saveHabitsData } from '@/app/actions/data'
|
import { addCoins, removeCoins, saveHabitsData } from '@/app/actions/data'
|
||||||
import { ToastAction } from '@/components/ui/toast'
|
import { ToastAction } from '@/components/ui/toast'
|
||||||
import { toast } from '@/hooks/use-toast'
|
import { toast } from '@/hooks/use-toast'
|
||||||
import { coinsAtom, currentUserAtom, habitFreqMapAtom, habitsAtom, settingsAtom, usersAtom } from '@/lib/atoms'
|
import { coinsAtom, currentUserAtom, habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||||
import { Habit } from '@/lib/types'
|
import { Freq, Habit } from '@/lib/types'
|
||||||
import {
|
import {
|
||||||
d2s,
|
d2s,
|
||||||
d2t,
|
d2t,
|
||||||
getCompletionsForDate,
|
getCompletionsForDate,
|
||||||
|
getHabitFreq,
|
||||||
getISODate,
|
getISODate,
|
||||||
getNow,
|
getNow,
|
||||||
getTodayInTimezone,
|
getTodayInTimezone,
|
||||||
@@ -23,12 +24,15 @@ import { useTranslations } from 'next-intl'
|
|||||||
export function useHabits() {
|
export function useHabits() {
|
||||||
const t = useTranslations('useHabits');
|
const t = useTranslations('useHabits');
|
||||||
const tCommon = useTranslations('Common');
|
const tCommon = useTranslations('Common');
|
||||||
const [usersData] = useAtom(usersAtom)
|
|
||||||
const [currentUser] = useAtom(currentUserAtom)
|
const [currentUser] = useAtom(currentUserAtom)
|
||||||
const [habitsData, setHabitsData] = useAtom(habitsAtom)
|
const [habitsData, setHabitsData] = useAtom(habitsAtom)
|
||||||
const [coins, setCoins] = useAtom(coinsAtom)
|
const [coins, setCoins] = useAtom(coinsAtom)
|
||||||
const [settings] = useAtom(settingsAtom)
|
const [settings] = useAtom(settingsAtom)
|
||||||
const [habitFreqMap] = useAtom(habitFreqMapAtom)
|
// const [habitFreqMap] = useAtom(habitFreqMapAtom)
|
||||||
|
const habitFreqMap = new Map<string, Freq>();
|
||||||
|
habitsData.habits.forEach(habit => {
|
||||||
|
habitFreqMap.set(habit.id, getHabitFreq(habit));
|
||||||
|
})
|
||||||
|
|
||||||
const completeHabit = async (habit: Habit) => {
|
const completeHabit = async (habit: Habit) => {
|
||||||
if (!handlePermissionCheck(currentUser, 'habit', 'interact', tCommon)) return
|
if (!handlePermissionCheck(currentUser, 'habit', 'interact', tCommon)) return
|
||||||
|
|||||||
118
lib/atoms.ts
118
lib/atoms.ts
@@ -3,10 +3,7 @@ import {
|
|||||||
calculateCoinsSpentToday,
|
calculateCoinsSpentToday,
|
||||||
calculateTotalEarned,
|
calculateTotalEarned,
|
||||||
calculateTotalSpent,
|
calculateTotalSpent,
|
||||||
calculateTransactionsToday,
|
|
||||||
generateCryptoHash,
|
generateCryptoHash,
|
||||||
getCompletionsForToday,
|
|
||||||
getHabitFreq,
|
|
||||||
isHabitDue,
|
isHabitDue,
|
||||||
prepareDataForHashing,
|
prepareDataForHashing,
|
||||||
roundToInteger,
|
roundToInteger,
|
||||||
@@ -16,9 +13,9 @@ import { atom } from "jotai";
|
|||||||
import { atomFamily, atomWithStorage } from "jotai/utils";
|
import { atomFamily, atomWithStorage } from "jotai/utils";
|
||||||
import { DateTime } from "luxon";
|
import { DateTime } from "luxon";
|
||||||
import {
|
import {
|
||||||
|
BrowserSettings,
|
||||||
CoinsData,
|
CoinsData,
|
||||||
CompletionCache,
|
CompletionCache,
|
||||||
Freq,
|
|
||||||
getDefaultCoinsData,
|
getDefaultCoinsData,
|
||||||
getDefaultHabitsData,
|
getDefaultHabitsData,
|
||||||
getDefaultServerSettings,
|
getDefaultServerSettings,
|
||||||
@@ -27,6 +24,7 @@ import {
|
|||||||
getDefaultWishlistData,
|
getDefaultWishlistData,
|
||||||
Habit,
|
Habit,
|
||||||
HabitsData,
|
HabitsData,
|
||||||
|
PomodoroAtom,
|
||||||
ServerSettings,
|
ServerSettings,
|
||||||
Settings,
|
Settings,
|
||||||
UserData,
|
UserData,
|
||||||
@@ -34,12 +32,6 @@ import {
|
|||||||
WishlistData
|
WishlistData
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
export interface BrowserSettings {
|
|
||||||
expandedHabits: boolean
|
|
||||||
expandedTasks: boolean
|
|
||||||
expandedWishlist: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const browserSettingsAtom = atomWithStorage('browserSettings', {
|
export const browserSettingsAtom = atomWithStorage('browserSettings', {
|
||||||
expandedHabits: false,
|
expandedHabits: false,
|
||||||
expandedTasks: false,
|
expandedTasks: false,
|
||||||
@@ -47,11 +39,21 @@ export const browserSettingsAtom = atomWithStorage('browserSettings', {
|
|||||||
} as BrowserSettings)
|
} as BrowserSettings)
|
||||||
|
|
||||||
export const usersAtom = atom(getDefaultUsersData<UserData>())
|
export const usersAtom = atom(getDefaultUsersData<UserData>())
|
||||||
|
export const currentUserIdAtom = atom<UserId | undefined>(undefined);
|
||||||
export const settingsAtom = atom(getDefaultSettings<Settings>());
|
export const settingsAtom = atom(getDefaultSettings<Settings>());
|
||||||
export const habitsAtom = atom(getDefaultHabitsData<HabitsData>());
|
export const habitsAtom = atom(getDefaultHabitsData<HabitsData>());
|
||||||
export const coinsAtom = atom(getDefaultCoinsData<CoinsData>());
|
export const coinsAtom = atom(getDefaultCoinsData<CoinsData>());
|
||||||
export const wishlistAtom = atom(getDefaultWishlistData<WishlistData>());
|
export const wishlistAtom = atom(getDefaultWishlistData<WishlistData>());
|
||||||
export const serverSettingsAtom = atom(getDefaultServerSettings<ServerSettings>());
|
export const serverSettingsAtom = atom(getDefaultServerSettings<ServerSettings>());
|
||||||
|
export const userSelectAtom = atom<boolean>(false)
|
||||||
|
export const aboutOpenAtom = atom<boolean>(false)
|
||||||
|
|
||||||
|
export const pomodoroAtom = atom<PomodoroAtom>({
|
||||||
|
show: false,
|
||||||
|
selectedHabitId: null,
|
||||||
|
autoStart: true,
|
||||||
|
minimized: false,
|
||||||
|
})
|
||||||
|
|
||||||
// Derived atom for coins earned today
|
// Derived atom for coins earned today
|
||||||
export const coinsEarnedTodayAtom = atom((get) => {
|
export const coinsEarnedTodayAtom = atom((get) => {
|
||||||
@@ -83,54 +85,12 @@ export const coinsSpentTodayAtom = atom((get) => {
|
|||||||
return roundToInteger(value);
|
return roundToInteger(value);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derived atom for transactions today
|
|
||||||
export const transactionsTodayAtom = atom((get) => {
|
|
||||||
const coins = get(coinsAtom);
|
|
||||||
const settings = get(settingsAtom);
|
|
||||||
return calculateTransactionsToday(coins.transactions, settings.system.timezone);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Atom to store the current logged-in user's ID.
|
|
||||||
// This should be set by your application when the user session is available.
|
|
||||||
export const currentUserIdAtom = atom<UserId | undefined>(undefined);
|
|
||||||
|
|
||||||
export const currentUserAtom = atom((get) => {
|
export const currentUserAtom = atom((get) => {
|
||||||
const currentUserId = get(currentUserIdAtom);
|
const currentUserId = get(currentUserIdAtom);
|
||||||
const users = get(usersAtom);
|
const users = get(usersAtom);
|
||||||
return users.users.find(user => user.id === currentUserId);
|
return users.users.find(user => user.id === currentUserId);
|
||||||
})
|
})
|
||||||
|
|
||||||
// Derived atom for current balance for the logged-in user
|
|
||||||
export const coinsBalanceAtom = atom((get) => {
|
|
||||||
const loggedInUserId = get(currentUserIdAtom);
|
|
||||||
if (!loggedInUserId) {
|
|
||||||
return 0; // No user logged in or ID not set, so balance is 0
|
|
||||||
}
|
|
||||||
const coins = get(coinsAtom);
|
|
||||||
const balance = coins.transactions
|
|
||||||
.filter(transaction => transaction.userId === loggedInUserId)
|
|
||||||
.reduce((sum, transaction) => sum + transaction.amount, 0);
|
|
||||||
return roundToInteger(balance);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* transient atoms */
|
|
||||||
interface PomodoroAtom {
|
|
||||||
show: boolean
|
|
||||||
selectedHabitId: string | null
|
|
||||||
autoStart: boolean
|
|
||||||
minimized: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const pomodoroAtom = atom<PomodoroAtom>({
|
|
||||||
show: false,
|
|
||||||
selectedHabitId: null,
|
|
||||||
autoStart: true,
|
|
||||||
minimized: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
export const userSelectAtom = atom<boolean>(false)
|
|
||||||
export const aboutOpenAtom = atom<boolean>(false)
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronous atom that calculates a freshness token (hash) based on the current client-side data.
|
* Asynchronous atom that calculates a freshness token (hash) based on the current client-side data.
|
||||||
* This token can be compared with a server-generated token to detect data discrepancies.
|
* This token can be compared with a server-generated token to detect data discrepancies.
|
||||||
@@ -147,34 +107,26 @@ export const clientFreshnessTokenAtom = atom(async (get) => {
|
|||||||
return hash;
|
return hash;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derived atom for completion cache
|
// Derived atom for completed habits by date, using the cache
|
||||||
export const completionCacheAtom = atom((get) => {
|
export const completedHabitsMapAtom = atom((get) => {
|
||||||
const habits = get(habitsAtom).habits;
|
const habits = get(habitsAtom).habits;
|
||||||
|
const completionCache: CompletionCache = {};
|
||||||
|
const map = new Map<string, Habit[]>();
|
||||||
const timezone = get(settingsAtom).system.timezone;
|
const timezone = get(settingsAtom).system.timezone;
|
||||||
const cache: CompletionCache = {};
|
|
||||||
|
|
||||||
habits.forEach(habit => {
|
habits.forEach(habit => {
|
||||||
habit.completions.forEach(utcTimestamp => {
|
habit.completions.forEach(utcTimestamp => {
|
||||||
const localDate = t2d({ timestamp: utcTimestamp, timezone })
|
const localDate = t2d({ timestamp: utcTimestamp, timezone })
|
||||||
.toFormat('yyyy-MM-dd');
|
.toFormat('yyyy-MM-dd');
|
||||||
|
|
||||||
if (!cache[localDate]) {
|
if (!completionCache[localDate]) {
|
||||||
cache[localDate] = {};
|
completionCache[localDate] = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
cache[localDate][habit.id] = (cache[localDate][habit.id] || 0) + 1;
|
completionCache[localDate][habit.id] = (completionCache[localDate][habit.id] || 0) + 1;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return cache;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Derived atom for completed habits by date, using the cache
|
|
||||||
export const completedHabitsMapAtom = atom((get) => {
|
|
||||||
const habits = get(habitsAtom).habits;
|
|
||||||
const completionCache = get(completionCacheAtom);
|
|
||||||
const map = new Map<string, Habit[]>();
|
|
||||||
|
|
||||||
// For each date in the cache
|
// For each date in the cache
|
||||||
Object.entries(completionCache).forEach(([dateKey, habitCompletions]) => {
|
Object.entries(completionCache).forEach(([dateKey, habitCompletions]) => {
|
||||||
const completedHabits = habits.filter(habit => {
|
const completedHabits = habits.filter(habit => {
|
||||||
@@ -191,38 +143,6 @@ export const completedHabitsMapAtom = atom((get) => {
|
|||||||
return map;
|
return map;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Derived atom for habit frequency map
|
|
||||||
export const habitFreqMapAtom = atom((get) => {
|
|
||||||
const habits = get(habitsAtom).habits;
|
|
||||||
const map = new Map<string, Freq>();
|
|
||||||
habits.forEach(habit => {
|
|
||||||
map.set(habit.id, getHabitFreq(habit));
|
|
||||||
});
|
|
||||||
return map;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const pomodoroTodayCompletionsAtom = atom((get) => {
|
|
||||||
const pomo = get(pomodoroAtom)
|
|
||||||
const habits = get(habitsAtom)
|
|
||||||
const settings = get(settingsAtom)
|
|
||||||
|
|
||||||
if (!pomo.selectedHabitId) return 0
|
|
||||||
|
|
||||||
const selectedHabit = habits.habits.find(h => h.id === pomo.selectedHabitId!)
|
|
||||||
if (!selectedHabit) return 0
|
|
||||||
|
|
||||||
return getCompletionsForToday({
|
|
||||||
habit: selectedHabit,
|
|
||||||
timezone: settings.system.timezone
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Derived atom to check if any habits are tasks
|
|
||||||
export const hasTasksAtom = atom((get) => {
|
|
||||||
const habits = get(habitsAtom)
|
|
||||||
return habits.habits.some(habit => habit.isTask === true)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Atom family for habits by specific date
|
// Atom family for habits by specific date
|
||||||
export const habitsByDateFamily = atomFamily((dateString: string) =>
|
export const habitsByDateFamily = atomFamily((dateString: string) =>
|
||||||
atom((get) => {
|
atom((get) => {
|
||||||
|
|||||||
13
lib/types.ts
13
lib/types.ts
@@ -210,3 +210,16 @@ export interface ParsedFrequencyResult {
|
|||||||
message: string | null
|
message: string | null
|
||||||
result: ParsedResultType
|
result: ParsedResultType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PomodoroAtom {
|
||||||
|
show: boolean
|
||||||
|
selectedHabitId: string | null
|
||||||
|
autoStart: boolean
|
||||||
|
minimized: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BrowserSettings {
|
||||||
|
expandedHabits: boolean
|
||||||
|
expandedTasks: boolean
|
||||||
|
expandedWishlist: boolean
|
||||||
|
}
|
||||||
16
lib/utils.ts
16
lib/utils.ts
@@ -1,5 +1,5 @@
|
|||||||
import { toast } from "@/hooks/use-toast"
|
import { toast } from "@/hooks/use-toast"
|
||||||
import { CoinsData, CoinTransaction, Freq, Habit, HabitsData, ParsedFrequencyResult, ParsedResultType, SafeUser, Settings, User, UserData, WishlistData } from '@/lib/types'
|
import { CoinsData, CoinTransaction, Freq, Habit, HabitsData, ParsedFrequencyResult, ParsedResultType, PomodoroAtom, SafeUser, Settings, User, UserData, WishlistData } from '@/lib/types'
|
||||||
import * as chrono from 'chrono-node'
|
import * as chrono from 'chrono-node'
|
||||||
import { clsx, type ClassValue } from "clsx"
|
import { clsx, type ClassValue } from "clsx"
|
||||||
import { DateTime, DateTimeFormatOptions } from "luxon"
|
import { DateTime, DateTimeFormatOptions } from "luxon"
|
||||||
@@ -84,6 +84,20 @@ export function getCompletionsForToday({
|
|||||||
return getCompletionsForDate({ habit, date: getTodayInTimezone(timezone), timezone })
|
return getCompletionsForDate({ habit, date: getTodayInTimezone(timezone), timezone })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTodayCompletions({ selectedHabitId }: PomodoroAtom, { habits }: HabitsData, { system: { timezone } }: Settings): number {
|
||||||
|
if (!selectedHabitId)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
const selectedHabit = habits.find(h => h.id === selectedHabitId!);
|
||||||
|
if (!selectedHabit)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
return getCompletionsForToday({
|
||||||
|
habit: selectedHabit,
|
||||||
|
timezone: timezone
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function getCompletedHabitsForDate({
|
export function getCompletedHabitsForDate({
|
||||||
habits,
|
habits,
|
||||||
date,
|
date,
|
||||||
|
|||||||
@@ -30,24 +30,7 @@ const nextConfig: NextConfig = {
|
|||||||
value: 'strict-origin-when-cross-origin',
|
value: 'strict-origin-when-cross-origin',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
}
|
||||||
{
|
|
||||||
source: '/sw.js',
|
|
||||||
headers: [
|
|
||||||
{
|
|
||||||
key: 'Content-Type',
|
|
||||||
value: 'application/javascript; charset=utf-8',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Cache-Control',
|
|
||||||
value: 'no-cache, no-store, must-revalidate',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'Content-Security-Policy',
|
|
||||||
value: "default-src 'self'; script-src 'self'",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
22
public/sw.js
22
public/sw.js
@@ -1,22 +0,0 @@
|
|||||||
self.addEventListener('push', function (event) {
|
|
||||||
if (event.data) {
|
|
||||||
const data = event.data.json()
|
|
||||||
const options = {
|
|
||||||
body: data.body,
|
|
||||||
icon: data.icon || '/icon.png',
|
|
||||||
badge: '/badge.png',
|
|
||||||
vibrate: [100, 50, 100],
|
|
||||||
data: {
|
|
||||||
dateOfArrival: Date.now(),
|
|
||||||
primaryKey: '2',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
event.waitUntil(self.registration.showNotification(data.title, options))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
self.addEventListener('notificationclick', function (event) {
|
|
||||||
console.log('Notification click received.')
|
|
||||||
event.notification.close()
|
|
||||||
event.waitUntil(clients.openWindow('/'))
|
|
||||||
})
|
|
||||||
Reference in New Issue
Block a user