diff --git a/frontend/src/components/calendar/CalendarPage.tsx b/frontend/src/components/calendar/CalendarPage.tsx index e9d490c..6dd9449 100644 --- a/frontend/src/components/calendar/CalendarPage.tsx +++ b/frontend/src/components/calendar/CalendarPage.tsx @@ -495,7 +495,7 @@ export default function CalendarPage() {
{/* Custom toolbar */} -
+
@@ -511,15 +511,17 @@ export default function CalendarPage() { Today - +
+ +
{(Object.entries(viewLabels) as [CalendarView, string][]).map(([view, label]) => ( diff --git a/frontend/src/components/dashboard/CalendarWidget.tsx b/frontend/src/components/dashboard/CalendarWidget.tsx index 05732b6..fc40716 100644 --- a/frontend/src/components/dashboard/CalendarWidget.tsx +++ b/frontend/src/components/dashboard/CalendarWidget.tsx @@ -50,8 +50,10 @@ export default function CalendarWidget({ events }: CalendarWidgetProps) { return () => clearInterval(interval); }, []); + const hasCurrentEvent = events.some((e) => getEventTimeState(e, clientNow) === 'current'); + return ( - +
diff --git a/frontend/src/components/dashboard/DashboardPage.tsx b/frontend/src/components/dashboard/DashboardPage.tsx index f1b3855..7aa0f44 100644 --- a/frontend/src/components/dashboard/DashboardPage.tsx +++ b/frontend/src/components/dashboard/DashboardPage.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery, useQueryClient } from '@tanstack/react-query'; -import { format, formatDistanceToNow } from 'date-fns'; +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'; @@ -42,6 +42,38 @@ export default function DashboardPage() { 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(() => { @@ -160,7 +192,12 @@ export default function DashboardPage() { } const updatedAgo = dataUpdatedAt - ? formatDistanceToNow(new Date(dataUpdatedAt), { addSuffix: true }) + ? (() => { + 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 ( @@ -173,7 +210,9 @@ export default function DashboardPage() {

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

{updatedAgo && ( <> diff --git a/frontend/src/components/dashboard/TodoWidget.tsx b/frontend/src/components/dashboard/TodoWidget.tsx index 827d9a0..777a5ef 100644 --- a/frontend/src/components/dashboard/TodoWidget.tsx +++ b/frontend/src/components/dashboard/TodoWidget.tsx @@ -48,8 +48,10 @@ export default function TodoWidget({ todos }: TodoWidgetProps) { onError: () => toast.error('Failed to complete todo'), }); + const hasOverdue = todos.some((t) => isPast(endOfDay(new Date(t.due_date)))); + return ( - +
diff --git a/frontend/src/components/dashboard/UpcomingWidget.tsx b/frontend/src/components/dashboard/UpcomingWidget.tsx index 069031d..10fd13f 100644 --- a/frontend/src/components/dashboard/UpcomingWidget.tsx +++ b/frontend/src/components/dashboard/UpcomingWidget.tsx @@ -30,6 +30,8 @@ const typeConfig: Record toggleDay(dateKey)} className={cn( - 'sticky top-0 bg-card z-10 w-full flex items-center gap-1.5 pb-1.5 border-b border-border cursor-pointer select-none', - groupIdx === 0 ? 'pt-0' : 'pt-3' + 'sticky top-0 z-10 w-full flex items-center gap-1.5 py-0.5 border-b border-border cursor-pointer select-none leading-none', + groupIdx === 0 ? 'pt-0' : 'mt-2' )} > {label} {isCollapsed && ( - + {dayItems.length} {dayItems.length === 1 ? 'item' : 'items'} )} diff --git a/frontend/src/components/layout/AppAmbientBackground.tsx b/frontend/src/components/layout/AppAmbientBackground.tsx new file mode 100644 index 0000000..67cf421 --- /dev/null +++ b/frontend/src/components/layout/AppAmbientBackground.tsx @@ -0,0 +1,52 @@ +/** + * Global ambient background effect. + * + * Uses CSS radial gradients with animated position shifting to create + * a living, breathing atmosphere behind all page content. + * The accent color is read from CSS custom properties so it adapts to any theme. + * + * Also renders a noise texture for tactile depth and a radial vignette + * to darken edges and draw focus to center content. + */ + +const NOISE_SVG = `url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E")`; + +export default function AppAmbientBackground() { + return ( +