import { Habit, SafeUser, User, Permission } from '@/lib/types'
import { useAtom } from 'jotai'
import { settingsAtom, pomodoroAtom, browserSettingsAtom, usersAtom } from '@/lib/atoms'
import { getTodayInTimezone, isSameDate, t2d, d2t, getNow, d2s, getCompletionsForToday, isTaskOverdue, convertMachineReadableFrequencyToHumanReadable } 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, MoreVertical, Timer, Archive, ArchiveRestore, Calendar, Pin } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { useEffect, useState } from 'react'
import { useHabits } from '@/hooks/useHabits'
import { INITIAL_RECURRENCE_RULE, RECURRENCE_RULE_MAP } from '@/lib/constants'
import { DateTime } from 'luxon'
import { Avatar, AvatarFallback, AvatarImage } from './ui/avatar'
import { useHelpers } from '@/lib/client-helpers'
interface HabitItemProps {
habit: Habit
onEdit: () => void
onDelete: () => void
}
const renderUserAvatars = (habit: Habit, currentUser: User | null, usersData: { users: User[] }) => {
if (!habit.userIds || habit.userIds.length <= 1) return null;
return (
{habit.userIds?.filter((u) => u !== currentUser?.id).map(userId => {
const user = usersData.users.find(u => u.id === userId)
if (!user) return null
return (
{user.username[0]}
)
})}
);
};
export default function HabitItem({ habit, onEdit, onDelete }: HabitItemProps) {
const { completeHabit, undoComplete, archiveHabit, unarchiveHabit, saveHabit } = useHabits()
const [settings] = useAtom(settingsAtom)
const [_, setPomo] = useAtom(pomodoroAtom)
const completionsToday = getCompletionsForToday({ habit, timezone: settings.system.timezone })
const target = habit.targetCompletions || 1
const isCompletedToday = completionsToday >= target
const [isHighlighted, setIsHighlighted] = useState(false)
const [usersData] = useAtom(usersAtom)
const { currentUser, hasPermission } = useHelpers()
const canWrite = hasPermission('habit', 'write')
const canInteract = hasPermission('habit', 'interact')
const [browserSettings] = useAtom(browserSettingsAtom)
const isTasksView = browserSettings.viewType === 'tasks'
const isRecurRule = !isTasksView
useEffect(() => {
const params = new URLSearchParams(window.location.search)
const highlightId = params.get('highlight')
if (highlightId === habit.id) {
setIsHighlighted(true)
// Scroll the element into view after a short delay to ensure rendering
setTimeout(() => {
const element = document.getElementById(`habit-${habit.id}`)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
}
}, 100)
// Remove highlight after animation
const timer = setTimeout(() => setIsHighlighted(false), 2000)
return () => clearTimeout(timer)
}
}, [habit.id])
return (
{habit.pinned && (
)}
{habit.name}
{isTaskOverdue(habit, settings.system.timezone) && (
Overdue
)}
{renderUserAvatars(habit, currentUser as User, usersData)}
{habit.description && (
{habit.description}
)}
When: {convertMachineReadableFrequencyToHumanReadable({
frequency: habit.frequency,
isRecurRule,
timezone: settings.system.timezone
})}
{habit.coinReward} coins per completion
{completionsToday > 0 && !habit.archived && (
)}
{!habit.archived && (
)}
{!habit.archived && (
{
if (!canInteract) return
setPomo((prev) => ({
...prev,
show: true,
selectedHabitId: habit.id
}))
}}>
Start Pomodoro
)}
{!habit.archived && (
<>
{habit.isTask && (
{
saveHabit({...habit, frequency: d2t({ dateTime: getNow({ timezone: settings.system.timezone })})})
}}>
Move to Today
)}
saveHabit({...habit, pinned: !habit.pinned})}>
{habit.pinned ? (
<>
Unpin
>
) : (
<>
Pin
>
)}
archiveHabit(habit.id)}>
Archive
>
)}
{habit.archived && (
unarchiveHabit(habit.id)}>
Unarchive
)}
Edit
Delete
)
}