import { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { format } from 'date-fns'; import { Bell, Plus, Calendar as CalIcon, ListTodo, RefreshCw } from 'lucide-react'; import api from '@/lib/api'; import type { DashboardData, UpcomingResponse, WeatherData } from '@/types'; import { useSettings } from '@/hooks/useSettings'; import { useAlerts } from '@/hooks/useAlerts'; import StatsWidget from './StatsWidget'; import TodoWidget from './TodoWidget'; import CalendarWidget from './CalendarWidget'; import UpcomingWidget from './UpcomingWidget'; import WeekTimeline from './WeekTimeline'; import DayBriefing from './DayBriefing'; import CountdownWidget from './CountdownWidget'; import TrackedProjectsWidget from './TrackedProjectsWidget'; import AlertBanner from './AlertBanner'; import EventForm from '../calendar/EventForm'; import TodoForm from '../todos/TodoForm'; import ReminderForm from '../reminders/ReminderForm'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { DashboardSkeleton } from '@/components/ui/skeleton'; import { cn } from '@/lib/utils'; function getGreeting(name?: string): string { const hour = new Date().getHours(); const suffix = name ? `, ${name}.` : '.'; if (hour < 5) return `Good night${suffix}`; if (hour < 12) return `Good morning${suffix}`; if (hour < 17) return `Good afternoon${suffix}`; if (hour < 21) return `Good evening${suffix}`; return `Good night${suffix}`; } export default function DashboardPage() { const navigate = useNavigate(); const queryClient = useQueryClient(); const { settings } = useSettings(); const { alerts, dismiss: dismissAlert, snooze: snoozeAlert } = useAlerts(); const [quickAddType, setQuickAddType] = useState<'event' | 'todo' | 'reminder' | null>(null); const [dropdownOpen, setDropdownOpen] = useState(false); const dropdownRef = useRef(null); const [clockNow, setClockNow] = useState(() => new Date()); // Live clock — synced to the minute boundary, re-syncs after tab sleep/resume useEffect(() => { let intervalId: ReturnType; let timeoutId: ReturnType; function startClock() { clearTimeout(timeoutId); clearInterval(intervalId); setClockNow(new Date()); const msUntilNextMinute = (60 - new Date().getSeconds()) * 1000 - new Date().getMilliseconds(); timeoutId = setTimeout(() => { setClockNow(new Date()); intervalId = setInterval(() => setClockNow(new Date()), 60_000); }, msUntilNextMinute); } startClock(); // Re-sync when tab becomes visible again (after sleep/background throttle) function handleVisibility() { if (document.visibilityState === 'visible') startClock(); } document.addEventListener('visibilitychange', handleVisibility); return () => { clearTimeout(timeoutId); clearInterval(intervalId); document.removeEventListener('visibilitychange', handleVisibility); }; }, []); // Click outside to close dropdown useEffect(() => { function handleClickOutside(e: MouseEvent) { if (dropdownRef.current && !dropdownRef.current.contains(e.target as Node)) { setDropdownOpen(false); } } if (dropdownOpen) document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, [dropdownOpen]); // Keyboard quick-add: Ctrl+N / Cmd+N opens dropdown, e/t/r selects type useEffect(() => { function handleKeyDown(e: KeyboardEvent) { // Don't trigger inside inputs/textareas or when a form is open const tag = (e.target as HTMLElement)?.tagName; if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return; if (quickAddType) return; if ((e.ctrlKey || e.metaKey) && e.key === 'n') { e.preventDefault(); setDropdownOpen(true); return; } if (dropdownOpen) { if (e.key === 'Escape') { setDropdownOpen(false); return; } if (e.key === 'e') { setQuickAddType('event'); setDropdownOpen(false); return; } if (e.key === 't') { setQuickAddType('todo'); setDropdownOpen(false); return; } if (e.key === 'r') { setQuickAddType('reminder'); setDropdownOpen(false); return; } } } document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [dropdownOpen, quickAddType]); const { data, isLoading, dataUpdatedAt } = useQuery({ queryKey: ['dashboard'], queryFn: async () => { const now = new Date(); const today = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; const { data } = await api.get(`/dashboard?client_date=${today}`); return data; }, staleTime: 60_000, refetchInterval: 120_000, }); const { data: upcomingData } = useQuery({ queryKey: ['upcoming', settings?.upcoming_days], queryFn: async () => { const days = settings?.upcoming_days || 7; const now = new Date(); const clientDate = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')}`; const { data } = await api.get(`/upcoming?days=${days}&client_date=${clientDate}`); return data; }, staleTime: 60_000, refetchInterval: 120_000, }); const { data: weatherData } = useQuery({ queryKey: ['weather'], queryFn: async () => { const { data } = await api.get('/weather'); return data; }, staleTime: 30 * 60 * 1000, retry: false, enabled: !!(settings?.weather_city || (settings?.weather_lat != null && settings?.weather_lon != null)), }); const handleRefresh = useCallback(() => { queryClient.invalidateQueries({ queryKey: ['dashboard'] }); queryClient.invalidateQueries({ queryKey: ['upcoming'] }); }, [queryClient]); if (isLoading) { return (
); } if (!data) { return (
Failed to load dashboard
); } const updatedAgo = dataUpdatedAt ? (() => { const mins = Math.floor((clockNow.getTime() - dataUpdatedAt) / 60_000); if (mins < 1) return 'just now'; if (mins === 1) return '1 min ago'; return `${mins} min ago`; })() : null; return (
{/* Header — greeting + date + quick add */}

{getGreeting(settings?.preferred_name || undefined)}

{format(clockNow, 'h:mm a')} | {format(clockNow, 'EEEE, MMMM d, yyyy')}

{updatedAgo && ( <> · Updated {updatedAgo} )}
{dropdownOpen && (
)}
{/* Week Timeline */} {upcomingData && (
)} {/* Smart Briefing */} {upcomingData && ( )} {/* Stats Row */}
{/* Alert Banner */} {/* Main Content — 2 columns */}
{/* Left: Upcoming feed (wider) */}
{/* Right: Countdown + Today's events + todos stacked */}
{data.starred_events.length > 0 && ( )}
{/* Active Reminders — exclude those already shown in alert banner */} {(() => { const alertIds = new Set(alerts.map((a) => a.id)); const futureReminders = data.active_reminders.filter((r) => !alertIds.has(r.id)); if (futureReminders.length === 0) return null; return (
Active Reminders
{futureReminders.map((reminder) => (
navigate('/reminders', { state: { reminderId: reminder.id } })} className="flex items-center gap-2 py-1.5 px-2 rounded-md hover:bg-card-elevated transition-colors duration-150 cursor-pointer" >
{reminder.title} {reminder.remind_at ? format(new Date(reminder.remind_at), 'MMM d, h:mm a') : ''}
))}
); })()} {/* Tracked Projects */}
{/* Quick Add Forms */} {quickAddType === 'event' && setQuickAddType(null)} />} {quickAddType === 'todo' && setQuickAddType(null)} />} {quickAddType === 'reminder' && setQuickAddType(null)} />}
); }