import { useState, useEffect, useMemo, useRef } from 'react'
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
import { motion, AnimatePresence } from 'framer-motion'
import { Check, Flame, TrendingUp, Zap, Sparkles, Undo2, Plus, Calendar, AlertTriangle, LogOut, Snowflake } from 'lucide-react'
import { format, startOfWeek, differenceInDays, parseISO, isToday, isTomorrow, isPast, startOfDay, isBefore, isAfter } from 'date-fns'
import { ru } from 'date-fns/locale'
import { habitsApi } from '../api/habits'
import { tasksApi } from '../api/tasks'
import { financeApi } from '../api/finance'
import { useAuthStore } from '../store/auth'
import Navigation from '../components/Navigation'
import CreateTaskModal from '../components/CreateTaskModal'
import LogHabitModal from '../components/LogHabitModal'
import clsx from 'clsx'
// Check if habit is frozen on a specific date
function isHabitFrozenOnDate(habit, freezes, date) {
if (!freezes || freezes.length === 0) return false
const checkDate = startOfDay(date)
return freezes.some(freeze => {
const start = startOfDay(parseISO(freeze.start_date))
const end = startOfDay(parseISO(freeze.end_date))
return !isBefore(checkDate, start) && !isAfter(checkDate, end)
})
}
// Определение "сегодняшних" привычек
function shouldShowToday(habit, lastLogDate, freezes) {
const today = startOfDay(new Date())
const dayOfWeek = today.getDay() || 7
if (isHabitFrozenOnDate(habit, freezes, today)) return false
const startDate = habit.start_date
? startOfDay(parseISO(habit.start_date))
: startOfDay(parseISO(habit.created_at))
if (today < startDate) return false
if (habit.frequency === "daily") return true
if (habit.frequency === "weekly") {
if (habit.target_days && habit.target_days.length > 0) {
return habit.target_days.includes(dayOfWeek)
}
if (!lastLogDate) return true
const weekStart = startOfWeek(today, { weekStartsOn: 1 })
const lastLog = typeof lastLogDate === "string" ? parseISO(lastLogDate) : lastLogDate
return lastLog < weekStart
}
if (habit.frequency === "interval" && habit.target_count > 0) {
const daysSinceStart = differenceInDays(today, startDate)
return daysSinceStart % habit.target_count === 0
}
if (habit.frequency === "custom" && habit.target_count > 0) {
if (!lastLogDate) return today >= startDate
const lastLog = typeof lastLogDate === "string" ? parseISO(lastLogDate) : lastLogDate
const daysSinceLastLog = differenceInDays(today, startOfDay(lastLog))
return daysSinceLastLog >= habit.target_count
}
return true
}
function formatDueDate(dateStr) {
if (!dateStr) return null
const date = parseISO(dateStr)
if (isToday(date)) return 'Сегодня'
if (isTomorrow(date)) return 'Завтра'
return format(date, 'd MMM', { locale: ru })
}
export default function Home() {
const [todayLogs, setTodayLogs] = useState({})
const [lastLogDates, setLastLogDates] = useState({})
const [habitFreezes, setHabitFreezes] = useState({})
const [habitLogs, setHabitLogs] = useState({})
const [showCreateTask, setShowCreateTask] = useState(false)
const [logHabitModal, setLogHabitModal] = useState({ open: false, habit: null })
const queryClient = useQueryClient()
const { user, logout } = useAuthStore()
const { data: habits = [], isLoading: habitsLoading } = useQuery({
queryKey: ['habits'],
queryFn: habitsApi.list,
})
const { data: stats } = useQuery({
queryKey: ['stats'],
queryFn: habitsApi.getStats,
})
const { data: todayTasks = [], isLoading: tasksLoading } = useQuery({
queryKey: ['tasks-today'],
queryFn: tasksApi.today,
})
const { data: financeSummary } = useQuery({
queryKey: ["finance-summary"],
queryFn: () => financeApi.getSummary(),
})
useEffect(() => {
if (habits.length > 0) {
loadTodayLogs()
loadHabitFreezes()
}
}, [habits])
const loadTodayLogs = async () => {
const today = format(new Date(), 'yyyy-MM-dd')
const logsMap = {}
const lastDates = {}
const allLogs = {}
await Promise.all(habits.map(async (habit) => {
try {
const logs = await habitsApi.getLogs(habit.id, 90)
allLogs[habit.id] = logs.map(l => l.date)
if (logs.length > 0) {
const lastLog = logs[0]
const logDate = lastLog.date.split('T')[0]
lastDates[habit.id] = logDate
if (logDate === today) logsMap[habit.id] = lastLog.id
}
} catch (e) {
console.error('Error loading logs for habit', habit.id, e)
}
}))
setTodayLogs(logsMap)
setLastLogDates(lastDates)
setHabitLogs(allLogs)
}
const loadHabitFreezes = async () => {
const freezesMap = {}
await Promise.all(habits.map(async (habit) => {
try {
const freezes = await habitsApi.getFreezes(habit.id)
freezesMap[habit.id] = freezes
} catch (e) {
freezesMap[habit.id] = []
}
}))
setHabitFreezes(freezesMap)
}
const logMutation = useMutation({
mutationFn: ({ habitId, date }) => habitsApi.log(habitId, date ? { date } : {}),
onSuccess: (data, { habitId, date }) => {
const logDate = date || format(new Date(), 'yyyy-MM-dd')
const today = format(new Date(), 'yyyy-MM-dd')
if (logDate === today) setTodayLogs(prev => ({ ...prev, [habitId]: data.id }))
setLastLogDates(prev => ({ ...prev, [habitId]: logDate }))
setHabitLogs(prev => ({ ...prev, [habitId]: [...(prev[habitId] || []), logDate] }))
queryClient.invalidateQueries({ queryKey: ['habits'] })
queryClient.invalidateQueries({ queryKey: ['stats'] })
},
})
const deleteLogMutation = useMutation({
mutationFn: ({ habitId, logId }) => habitsApi.deleteLog(habitId, logId),
onSuccess: (_, { habitId }) => {
setTodayLogs(prev => {
const newLogs = { ...prev }
delete newLogs[habitId]
return newLogs
})
loadTodayLogs()
queryClient.invalidateQueries({ queryKey: ['habits'] })
queryClient.invalidateQueries({ queryKey: ['stats'] })
},
})
const completeTaskMutation = useMutation({
mutationFn: (id) => tasksApi.complete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks-today'] })
queryClient.invalidateQueries({ queryKey: ['tasks'] })
},
})
const uncompleteTaskMutation = useMutation({
mutationFn: (id) => tasksApi.uncomplete(id),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tasks-today'] })
queryClient.invalidateQueries({ queryKey: ['tasks'] })
},
})
const handleToggleComplete = (habitId) => {
if (todayLogs[habitId]) {
deleteLogMutation.mutate({ habitId, logId: todayLogs[habitId] })
} else {
logMutation.mutate({ habitId })
}
}
const handleLogHabitDate = async (habitId, date) => {
await logMutation.mutateAsync({ habitId, date })
}
const handleToggleTask = (task) => {
if (task.completed) uncompleteTaskMutation.mutate(task.id)
else completeTaskMutation.mutate(task.id)
}
const todayHabits = useMemo(() => {
return habits.filter(habit => shouldShowToday(habit, lastLogDates[habit.id], habitFreezes[habit.id]))
}, [habits, lastLogDates, habitFreezes])
const frozenHabits = useMemo(() => {
const today = startOfDay(new Date())
return habits.filter(habit => isHabitFrozenOnDate(habit, habitFreezes[habit.id], today))
}, [habits, habitFreezes])
const completedCount = Object.keys(todayLogs).length
const totalToday = todayHabits.length
const today = format(new Date(), 'EEEE, d MMMM', { locale: ru })
const activeTasks = todayTasks.filter(t => !t.completed)
return (
Привет, {user?.username}!
{today}
{/* Progress */}
Прогресс на сегодня
{completedCount} / {totalToday}
0 ? `${(completedCount / totalToday) * 100}%` : '0%' }}
transition={{ duration: 0.5, ease: 'easeOut' }}
className="h-full bg-gradient-to-r from-primary-500 to-accent-500 rounded-full"
/>
{completedCount === totalToday && totalToday > 0 && (
🎉 Все привычки выполнены!
)}
{frozenHabits.length > 0 && (
{frozenHabits.length} привычек на паузе
)}
{/* Stats */}
{stats && (
{stats.today_completed}
Выполнено
{stats.active_habits}
Активных
)}
{/* Tasks */}
{/* Finance Summary */}
{financeSummary && (
💰 Баланс
+{(financeSummary.total_income || 0).toLocaleString("ru-RU")} ₽
Доходы
-{(financeSummary.total_expense || 0).toLocaleString("ru-RU")} ₽
Расходы
= 0 ? "text-primary-500" : "text-red-500")}>{(financeSummary.balance || 0).toLocaleString("ru-RU")} ₽
Баланс
)}
{(activeTasks.length > 0 || !tasksLoading) && (
Задачи на сегодня
{tasksLoading ? (
) : activeTasks.length === 0 ? (
Нет задач на сегодня
) : (
{activeTasks.map((task, index) => (
handleToggleTask(task)} isLoading={completeTaskMutation.isPending || uncompleteTaskMutation.isPending} />
))}
)}
)}
{/* Habits */}
Привычки
{habitsLoading ? (
{[1, 2, 3].map((i) => (
))}
) : todayHabits.length === 0 ? (
Свободный день!
На сегодня нет запланированных привычек.
) : (
{todayHabits.map((habit, index) => (
handleToggleComplete(habit.id)}
onLongPress={() => setLogHabitModal({ open: true, habit })}
isLoading={logMutation.isPending || deleteLogMutation.isPending}
/>
))}
)}
setShowCreateTask(false)} />
setLogHabitModal({ open: false, habit: null })}
habit={logHabitModal.habit}
completedDates={habitLogs[logHabitModal.habit?.id] || []}
onLogDate={handleLogHabitDate}
/>
)
}
function TaskCard({ task, index, onToggle, isLoading }) {
const [showConfetti, setShowConfetti] = useState(false)
const dueDateLabel = formatDueDate(task.due_date)
const isOverdue = task.due_date && isPast(parseISO(task.due_date)) && !isToday(parseISO(task.due_date)) && !task.completed
const handleCheck = (e) => {
e.stopPropagation()
if (isLoading) return
if (!task.completed) {
setShowConfetti(true)
setTimeout(() => setShowConfetti(false), 1000)
}
onToggle()
}
return (
{showConfetti && (
{[...Array(6)].map((_, i) => (
))}
)}
{task.completed ? (
) : (
{task.icon || '📋'}
)}
{task.title}
{(dueDateLabel || isOverdue) && (
{isOverdue && }
{dueDateLabel}
)}
{task.completed && (
)}
)
}
function HabitCard({ habit, index, isCompleted, onToggle, onLongPress, isLoading }) {
const [showConfetti, setShowConfetti] = useState(false)
const longPressTimer = useRef(null)
const isLongPress = useRef(false)
const handleTouchStart = () => {
isLongPress.current = false
longPressTimer.current = setTimeout(() => { isLongPress.current = true; onLongPress() }, 500)
}
const handleTouchEnd = () => { if (longPressTimer.current) clearTimeout(longPressTimer.current) }
const handleCheck = (e) => {
e.stopPropagation()
if (isLoading || isLongPress.current) return
if (!isCompleted) { setShowConfetti(true); setTimeout(() => setShowConfetti(false), 1000) }
onToggle()
}
const handleContextMenu = (e) => { e.preventDefault(); onLongPress() }
return (
{showConfetti && (
{[...Array(6)].map((_, i) => (
))}
)}
{isCompleted ? (
) : (
{habit.icon || '✨'}
)}
{habit.name}
{habit.description &&
{habit.description}
}
{isCompleted && (
)}
)
}