mirror of
https://github.com/ManInDark/HabitTrove.git
synced 2026-01-21 06:34:30 +01:00
added habit daily completion target (#26)
This commit is contained in:
@@ -1,10 +1,17 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState, useEffect, useRef } from 'react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip'
|
||||
import { Info, SmilePlus } from 'lucide-react'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import data from '@emoji-mart/data'
|
||||
import Picker from '@emoji-mart/react'
|
||||
import { Habit } from '@/lib/types'
|
||||
|
||||
interface AddEditHabitModalProps {
|
||||
@@ -15,10 +22,12 @@ interface AddEditHabitModalProps {
|
||||
}
|
||||
|
||||
export default function AddEditHabitModal({ isOpen, onClose, onSave, habit }: AddEditHabitModalProps) {
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [frequency, setFrequency] = useState<'daily' | 'weekly' | 'monthly'>('daily')
|
||||
const [coinReward, setCoinReward] = useState(1)
|
||||
const [targetCompletions, setTargetCompletions] = useState(1)
|
||||
|
||||
useEffect(() => {
|
||||
if (habit) {
|
||||
@@ -26,6 +35,7 @@ export default function AddEditHabitModal({ isOpen, onClose, onSave, habit }: Ad
|
||||
setDescription(habit.description)
|
||||
setFrequency(habit.frequency)
|
||||
setCoinReward(habit.coinReward)
|
||||
setTargetCompletions(habit.targetCompletions || 1)
|
||||
} else {
|
||||
setName('')
|
||||
setDescription('')
|
||||
@@ -36,7 +46,14 @@ export default function AddEditHabitModal({ isOpen, onClose, onSave, habit }: Ad
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
onSave({ name, description, frequency, coinReward, completions: habit?.completions || [] })
|
||||
onSave({
|
||||
name,
|
||||
description,
|
||||
frequency,
|
||||
coinReward,
|
||||
targetCompletions: targetCompletions > 1 ? targetCompletions : undefined,
|
||||
completions: habit?.completions || []
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -51,13 +68,37 @@ export default function AddEditHabitModal({ isOpen, onClose, onSave, habit }: Ad
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="col-span-3"
|
||||
required
|
||||
/>
|
||||
<div className='flex col-span-3 gap-2'>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
/>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<SmilePlus className="h-8 w-8" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<Picker
|
||||
data={data}
|
||||
onEmojiSelect={(emoji: any) => {
|
||||
setName(prev => `${prev}${emoji.native}`)
|
||||
// Focus back on input after selection
|
||||
const input = document.getElementById('name') as HTMLInputElement
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="description" className="text-right">
|
||||
@@ -85,6 +126,42 @@ export default function AddEditHabitModal({ isOpen, onClose, onSave, habit }: Ad
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<div className="flex items-center gap-2 justify-end">
|
||||
<Label htmlFor="targetCompletions">
|
||||
Daily Target
|
||||
</Label>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Info className="h-4 w-4" />
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className='text-sm'>
|
||||
<p>How many times you want to complete this habit each day.<br />For example: drink 7 glasses of water or take 3 walks<br /><br />You'll only receive the coin reward after reaching the daily target.</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
<div className="col-span-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Input
|
||||
id="targetCompletions"
|
||||
type="number"
|
||||
value={targetCompletions}
|
||||
onChange={(e) => {
|
||||
const value = parseInt(e.target.value)
|
||||
setTargetCompletions(isNaN(value) ? 1 : Math.max(1, value))
|
||||
}}
|
||||
min={1}
|
||||
max={10}
|
||||
className="w-20"
|
||||
/>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
times per day
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="coinReward" className="text-right">
|
||||
Coin Reward
|
||||
|
||||
@@ -4,6 +4,10 @@ import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { SmilePlus } from 'lucide-react'
|
||||
import data from '@emoji-mart/data'
|
||||
import Picker from '@emoji-mart/react'
|
||||
import { WishlistItemType } from '@/lib/types'
|
||||
|
||||
interface AddEditWishlistItemModalProps {
|
||||
@@ -47,13 +51,38 @@ export default function AddEditWishlistItemModal({ isOpen, onClose, onSave, item
|
||||
<Label htmlFor="name" className="text-right">
|
||||
Name
|
||||
</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="col-span-3"
|
||||
required
|
||||
/>
|
||||
<div className="col-span-3 flex gap-2">
|
||||
<Input
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="flex-1"
|
||||
required
|
||||
/>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<SmilePlus className="h-4 w-4" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<Picker
|
||||
data={data}
|
||||
onEmojiSelect={(emoji: any) => {
|
||||
setName(prev => `${prev}${emoji.native}`)
|
||||
// Focus back on input after selection
|
||||
const input = document.getElementById('name') as HTMLInputElement
|
||||
input?.focus()
|
||||
}}
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-4 items-center gap-4">
|
||||
<Label htmlFor="description" className="text-right">
|
||||
|
||||
0
components/CoinBalance.test.tsx
Normal file
0
components/CoinBalance.test.tsx
Normal file
@@ -1,36 +1,37 @@
|
||||
import { Circle, Coins, ArrowRight, CircleCheck, ChevronDown, ChevronUp } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import { useAtom } from 'jotai'
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, getCompletedHabitsForDate, getCompletionsForDate } from '@/lib/utils'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { WishlistItemType } from '@/lib/types'
|
||||
import { Habit } from '@/lib/types'
|
||||
import Linkify from './linkify'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
|
||||
interface UpcomingItemsProps {
|
||||
habits: Habit[]
|
||||
wishlistItems: WishlistItemType[]
|
||||
coinBalance: number
|
||||
onComplete: (habit: Habit) => void
|
||||
onUndo: (habit: Habit) => void
|
||||
}
|
||||
|
||||
export default function DailyOverview({
|
||||
habits,
|
||||
wishlistItems,
|
||||
coinBalance,
|
||||
onComplete,
|
||||
onUndo
|
||||
}: UpcomingItemsProps) {
|
||||
const { completeHabit, undoComplete } = useHabits()
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const today = getTodayInTimezone(settings.system.timezone)
|
||||
const todayCompletions = habits.filter(habit =>
|
||||
habit.completions.includes(today)
|
||||
)
|
||||
const todayCompletions = getCompletedHabitsForDate({
|
||||
habits,
|
||||
date: getNow({ timezone: settings.system.timezone }),
|
||||
timezone: settings.system.timezone
|
||||
})
|
||||
|
||||
// Filter daily habits
|
||||
const dailyHabits = habits.filter(habit => habit.frequency === 'daily')
|
||||
@@ -54,7 +55,12 @@ export default function DailyOverview({
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="font-semibold">Daily Habits</h3>
|
||||
<Badge variant="secondary">
|
||||
{todayCompletions.length}/{dailyHabits.length} Complete
|
||||
{dailyHabits.reduce((sum, habit) => sum + getCompletionsForDate({
|
||||
habit,
|
||||
date: today,
|
||||
timezone: settings.system.timezone
|
||||
}), 0)}/
|
||||
{dailyHabits.reduce((sum, habit) => sum + (habit.targetCompletions || 1), 0)} Completions
|
||||
</Badge>
|
||||
</div>
|
||||
<ul className={`grid gap-2 transition-all duration-300 ease-in-out ${expandedHabits ? 'max-h-[500px] opacity-100' : 'max-h-[200px] opacity-100'} overflow-hidden`}>
|
||||
@@ -66,7 +72,11 @@ export default function DailyOverview({
|
||||
})
|
||||
.slice(0, expandedHabits ? undefined : 3)
|
||||
.map((habit) => {
|
||||
const isCompleted = todayCompletions.includes(habit)
|
||||
const completionsToday = habit.completions.filter(completion =>
|
||||
isSameDate(t2d({ timestamp: completion, timezone: settings.system.timezone }), t2d({ timestamp: d2t({ dateTime: getNow({ timezone: settings.system.timezone }) }), timezone: settings.system.timezone }))
|
||||
).length
|
||||
const target = habit.targetCompletions || 1
|
||||
const isCompleted = completionsToday >= target
|
||||
return (
|
||||
<li
|
||||
key={habit.id}
|
||||
@@ -78,17 +88,30 @@ export default function DailyOverview({
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
if (isCompleted) {
|
||||
onUndo(habit);
|
||||
undoComplete(habit);
|
||||
} else {
|
||||
onComplete(habit);
|
||||
completeHabit(habit);
|
||||
}
|
||||
}}
|
||||
className="hover:opacity-70 transition-opacity"
|
||||
className="relative hover:opacity-70 transition-opacity w-4 h-4"
|
||||
>
|
||||
{isCompleted ? (
|
||||
<CircleCheck className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Circle className="h-4 w-4" />
|
||||
<div className="relative h-4 w-4">
|
||||
<Circle className="absolute h-4 w-4 text-muted-foreground" />
|
||||
<div
|
||||
className="absolute h-4 w-4 rounded-full overflow-hidden"
|
||||
style={{
|
||||
background: `conic-gradient(
|
||||
currentColor ${(completionsToday / target) * 360}deg,
|
||||
transparent ${(completionsToday / target) * 360}deg 360deg
|
||||
)`,
|
||||
mask: 'radial-gradient(transparent 50%, black 51%)',
|
||||
WebkitMask: 'radial-gradient(transparent 50%, black 51%)'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<span className={isCompleted ? 'line-through' : ''}>
|
||||
@@ -97,9 +120,16 @@ export default function DailyOverview({
|
||||
</Linkify>
|
||||
</span>
|
||||
</span>
|
||||
<span className="flex items-center text-xs text-muted-foreground">
|
||||
<Coins className="h-3 w-3 text-yellow-400 mr-1" />
|
||||
{habit.coinReward}
|
||||
<span className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
{habit.targetCompletions && (
|
||||
<span className="bg-secondary px-1.5 py-0.5 rounded-full">
|
||||
{completionsToday}/{target}
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center">
|
||||
<Coins className="h-3 w-3 text-yellow-400 mr-1" />
|
||||
{habit.coinReward}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ import HabitStreak from './HabitStreak'
|
||||
import { useHabits } from '@/hooks/useHabits'
|
||||
|
||||
export default function Dashboard() {
|
||||
const { completeHabit, undoComplete } = useHabits()
|
||||
const [habitsData] = useAtom(habitsAtom)
|
||||
const habits = habitsData.habits
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
@@ -28,8 +27,6 @@ export default function Dashboard() {
|
||||
wishlistItems={wishlistItems}
|
||||
habits={habits}
|
||||
coinBalance={coinBalance}
|
||||
onComplete={completeHabit}
|
||||
onUndo={undoComplete}
|
||||
/>
|
||||
|
||||
{/* <HabitHeatmap habits={habits} /> */}
|
||||
|
||||
@@ -4,11 +4,12 @@ import { useState } from 'react'
|
||||
import { Calendar } from '@/components/ui/calendar'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { d2s, getNow } from '@/lib/utils'
|
||||
import { d2s, getNow, t2d, getCompletedHabitsForDate } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||
import { DateTime } from 'luxon'
|
||||
import Linkify from './linkify'
|
||||
import { Habit } from '@/lib/types'
|
||||
|
||||
export default function HabitCalendar() {
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
@@ -17,10 +18,11 @@ export default function HabitCalendar() {
|
||||
const habits = habitsData.habits
|
||||
|
||||
const getHabitsForDate = (date: Date) => {
|
||||
const dateString = date.toISOString().split('T')[0]
|
||||
return habits.filter(habit =>
|
||||
habit.completions.includes(dateString)
|
||||
)
|
||||
return getCompletedHabitsForDate({
|
||||
habits,
|
||||
date: DateTime.fromJSDate(date),
|
||||
timezone: settings.system.timezone
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -50,7 +52,7 @@ export default function HabitCalendar() {
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
{selectedDate ? (
|
||||
<>Habits for {d2s({ dateTime: selectedDate, timezone: settings.system.timezone })}</>
|
||||
<>Habits for {d2s({ dateTime: selectedDate, timezone: settings.system.timezone, format: "yyyy-MM-dd" })}</>
|
||||
) : (
|
||||
'Select a date'
|
||||
)}
|
||||
@@ -60,7 +62,7 @@ export default function HabitCalendar() {
|
||||
{selectedDate && (
|
||||
<ul className="space-y-2">
|
||||
{habits.map((habit) => {
|
||||
const isCompleted = getHabitsForDate(selectedDate.toJSDate()).some(h => h.id === habit.id)
|
||||
const isCompleted = getHabitsForDate(selectedDate.toJSDate()).some((h: Habit) => h.id === habit.id)
|
||||
return (
|
||||
<li key={habit.id} className="flex items-center justify-between">
|
||||
<span>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import HeatMap from '@uiw/react-heat-map'
|
||||
import { Habit } from '@/lib/types'
|
||||
import { getNow } from '@/lib/utils'
|
||||
import { getNow, d2s, t2d } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
|
||||
@@ -13,10 +13,21 @@ interface HabitHeatmapProps {
|
||||
export default function HabitHeatmap({ habits }: HabitHeatmapProps) {
|
||||
// Aggregate all habit completions into a count per day
|
||||
const completionCounts = habits.reduce((acc: { [key: string]: number }, habit) => {
|
||||
habit.completions.forEach(date => {
|
||||
// Convert date format from ISO (YYYY-MM-DD) to YYYY/MM/DD for the heatmap
|
||||
const formattedDate = date.replace(/-/g, '/')
|
||||
acc[formattedDate] = (acc[formattedDate] || 0) + 1
|
||||
const target = habit.targetCompletions || 1
|
||||
const dailyCompletions = habit.completions.reduce((dailyAcc, completion) => {
|
||||
const formattedDate = d2s({
|
||||
dateTime: t2d({ timestamp: completion, timezone: settings.system.timezone }),
|
||||
format: 'yyyy-MM-dd',
|
||||
timezone: settings.system.timezone
|
||||
})
|
||||
dailyAcc[formattedDate] = (dailyAcc[formattedDate] || 0) + 1
|
||||
return dailyAcc
|
||||
}, {} as { [key: string]: number })
|
||||
|
||||
Object.entries(dailyCompletions).forEach(([date, count]) => {
|
||||
if (count >= target) {
|
||||
acc[date] = (acc[date] || 0) + 1
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Habit } from '@/lib/types'
|
||||
import { useAtom } from 'jotai'
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow } 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'
|
||||
@@ -18,7 +18,11 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
const { completeHabit, undoComplete } = useHabits()
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const today = getTodayInTimezone(settings.system.timezone)
|
||||
const isCompletedToday = habit.completions?.includes(today)
|
||||
const completionsToday = habit.completions?.filter(completion =>
|
||||
isSameDate(t2d({ timestamp: completion, timezone: settings.system.timezone }), t2d({ timestamp: d2t({ dateTime: getNow({ timezone: settings.system.timezone }) }), timezone: settings.system.timezone }))
|
||||
).length || 0
|
||||
const target = habit.targetCompletions || 1
|
||||
const isCompletedToday = completionsToday >= target
|
||||
const [isHighlighted, setIsHighlighted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -68,16 +72,31 @@ export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant={isCompletedToday ? "secondary" : "default"}
|
||||
size="sm"
|
||||
onClick={async () => await completeHabit(habit)}
|
||||
disabled={isCompletedToday}
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
{isCompletedToday ? "Completed" : "Complete"}
|
||||
</Button>
|
||||
{isCompletedToday && (
|
||||
<div className="relative">
|
||||
<Button
|
||||
variant={isCompletedToday ? "secondary" : "default"}
|
||||
size="sm"
|
||||
onClick={async () => await completeHabit(habit)}
|
||||
disabled={isCompletedToday && completionsToday >= target}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<Check className="h-4 w-4 mr-2" />
|
||||
{isCompletedToday ? (
|
||||
target > 1 ? `Completed (${completionsToday}/${target})` : 'Completed'
|
||||
) : (
|
||||
target > 1 ? `Complete (${completionsToday}/${target})` : 'Complete'
|
||||
)}
|
||||
{habit.targetCompletions && habit.targetCompletions > 1 && (
|
||||
<div
|
||||
className="absolute bottom-0 left-0 h-1 bg-white/50"
|
||||
style={{
|
||||
width: `${(completionsToday / target) * 100}%`
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{completionsToday > 0 && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { BarChart } from 'lucide-react'
|
||||
import { getTodayInTimezone } from '@/lib/utils'
|
||||
import { getTodayInTimezone, getCompletedHabitsForDate } from '@/lib/utils'
|
||||
import { useAtom } from 'jotai'
|
||||
import { habitsAtom, settingsAtom } from '@/lib/atoms'
|
||||
|
||||
@@ -11,9 +11,11 @@ export default function HabitOverview() {
|
||||
const [settings] = useAtom(settingsAtom)
|
||||
const today = getTodayInTimezone(settings.system.timezone)
|
||||
|
||||
const completedToday = habits.filter(habit =>
|
||||
habit.completions.includes(today)
|
||||
).length
|
||||
const completedToday = getCompletedHabitsForDate({
|
||||
habits,
|
||||
date: today,
|
||||
timezone: settings.system.timezone
|
||||
}).length
|
||||
|
||||
const habitsByFrequency = habits.reduce((acc, habit) => ({
|
||||
...acc,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { Habit } from '@/lib/types'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { d2s, getNow } from '@/lib/utils'
|
||||
import { d2s, getNow, t2d, getCompletedHabitsForDate } from '@/lib/utils'
|
||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'
|
||||
import { useAtom } from 'jotai'
|
||||
import { settingsAtom } from '@/lib/atoms'
|
||||
@@ -20,9 +20,11 @@ export default function HabitStreak({ habits }: HabitStreakProps) {
|
||||
}).reverse()
|
||||
|
||||
const completions = dates.map(date => {
|
||||
const completedCount = habits.reduce((count, habit) => {
|
||||
return count + (habit.completions.includes(date) ? 1 : 0);
|
||||
}, 0);
|
||||
const completedCount = getCompletedHabitsForDate({
|
||||
habits,
|
||||
date: t2d({ timestamp: date, timezone: settings.system.timezone }),
|
||||
timezone: settings.system.timezone
|
||||
}).length;
|
||||
return {
|
||||
date,
|
||||
completed: completedCount
|
||||
|
||||
33
components/ui/popover.tsx
Normal file
33
components/ui/popover.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Popover = PopoverPrimitive.Root
|
||||
|
||||
const PopoverTrigger = PopoverPrimitive.Trigger
|
||||
|
||||
const PopoverAnchor = PopoverPrimitive.Anchor
|
||||
|
||||
const PopoverContent = React.forwardRef<
|
||||
React.ElementRef<typeof PopoverPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
|
||||
>(({ className, align = "center", sideOffset = 4, ...props }, ref) => (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Content
|
||||
ref={ref}
|
||||
align={align}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</PopoverPrimitive.Portal>
|
||||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
32
components/ui/tooltip.tsx
Normal file
32
components/ui/tooltip.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const TooltipProvider = TooltipPrimitive.Provider
|
||||
|
||||
const Tooltip = TooltipPrimitive.Root
|
||||
|
||||
const TooltipTrigger = TooltipPrimitive.Trigger
|
||||
|
||||
const TooltipContent = React.forwardRef<
|
||||
React.ElementRef<typeof TooltipPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
|
||||
>(({ className, sideOffset = 4, ...props }, ref) => (
|
||||
<TooltipPrimitive.Portal>
|
||||
<TooltipPrimitive.Content
|
||||
ref={ref}
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 overflow-hidden rounded-md bg-primary px-3 py-1.5 text-xs text-primary-foreground animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</TooltipPrimitive.Portal>
|
||||
))
|
||||
TooltipContent.displayName = TooltipPrimitive.Content.displayName
|
||||
|
||||
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
|
||||
Reference in New Issue
Block a user