Kyle Pope b41b0b6635 Add dashboard polish: micro-animations, visual upgrades, and interactivity
Batch 1+2 implementation (17 items): plus button rotation, card hover
glow consistency, DayBriefing container with Sparkles icon, WeekTimeline
hover scale + pulsing today dot + dot tooltips, countdown urgency scaling,
CalendarWidget time progress bar + current event highlight + empty state,
TodoWidget inline complete + empty state, dashboard auto-refresh (2min),
optimistic todo completion, "Updated Xm ago" with refresh button, keyboard
quick-add (Ctrl+N → e/t/r), progress rings on stat cards, staggered row
entrance in Upcoming, content crossfade, prefers-reduced-motion support,
ARIA attributes on dropdown menu, and hover:bg-card-elevated consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 00:02:04 +08:00

160 lines
6.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useMemo } from 'react';
import { format, isSameDay, startOfDay, addDays, isAfter } from 'date-fns';
import { Sparkles } from 'lucide-react';
import type { UpcomingItem, DashboardData } from '@/types';
interface DayBriefingProps {
upcomingItems: UpcomingItem[];
dashboardData: DashboardData;
weatherData?: { rain_chance: number; description: string } | null;
}
function getItemTime(item: UpcomingItem): string {
if (item.datetime) {
return format(new Date(item.datetime), 'h:mm a');
}
return '';
}
export default function DayBriefing({ upcomingItems, dashboardData, weatherData }: DayBriefingProps) {
const briefing = useMemo(() => {
const now = new Date();
const hour = now.getHours();
const today = startOfDay(now);
const tomorrow = addDays(today, 1);
const todayItems = upcomingItems.filter((item) => {
const d = item.datetime ? new Date(item.datetime) : new Date(item.date);
return isSameDay(startOfDay(d), today);
});
const tomorrowItems = upcomingItems.filter((item) => {
const d = item.datetime ? new Date(item.datetime) : new Date(item.date);
return isSameDay(startOfDay(d), tomorrow);
});
const todayEvents = dashboardData.todays_events;
const activeReminders = dashboardData.active_reminders;
const todayTodos = dashboardData.upcoming_todos.filter((t) => {
if (!t.due_date) return false;
return isSameDay(startOfDay(new Date(t.due_date)), today);
});
const parts: string[] = [];
// Night (9PM5AM): Show today if items remain, then preview tomorrow
if (hour >= 21 || hour < 5) {
// Before 5 AM, "today" still matters — mention remaining items
if (todayItems.length > 0 && hour < 5) {
const remainingToday = todayEvents.filter((e) => e.end_datetime && isAfter(new Date(e.end_datetime), now));
if (remainingToday.length > 0) {
parts.push(`${remainingToday.length} event${remainingToday.length > 1 ? 's' : ''} still on today.`);
} else if (todayTodos.length > 0) {
parts.push(`${todayTodos.length} task${todayTodos.length > 1 ? 's' : ''} due today.`);
} else if (activeReminders.length > 0) {
parts.push(`${activeReminders.length} reminder${activeReminders.length > 1 ? 's' : ''} set for today.`);
}
}
if (tomorrowItems.length === 0) {
parts.push('Tomorrow is clear — nothing scheduled.');
} else {
const firstEvent = tomorrowItems.find((i) => i.type === 'event' && i.datetime);
if (firstEvent) {
parts.push(
`You have ${tomorrowItems.length} item${tomorrowItems.length > 1 ? 's' : ''} tomorrow, starting with ${firstEvent.title} at ${getItemTime(firstEvent)}.`
);
} else {
parts.push(
`You have ${tomorrowItems.length} item${tomorrowItems.length > 1 ? 's' : ''} lined up for tomorrow.`
);
}
}
}
// Morning (5AM12PM)
else if (hour < 12) {
if (todayItems.length === 0) {
parts.push('Your day is wide open — no events or tasks scheduled.');
} else {
const eventCount = todayEvents.length;
const todoCount = todayTodos.length;
const segments: string[] = [];
if (eventCount > 0) segments.push(`${eventCount} event${eventCount > 1 ? 's' : ''}`);
if (todoCount > 0) segments.push(`${todoCount} task${todoCount > 1 ? 's' : ''} due`);
if (segments.length > 0) {
parts.push(`Today: ${segments.join(' and ')}.`);
}
const firstEvent = todayEvents.find((e) => {
const d = new Date(e.start_datetime);
return isAfter(d, now);
});
if (firstEvent) {
parts.push(`Up next is ${firstEvent.title} at ${format(new Date(firstEvent.start_datetime), 'h:mm a')}.`);
}
}
}
// Afternoon (12PM5PM)
else if (hour < 17) {
const remainingEvents = todayEvents.filter((e) => e.end_datetime && isAfter(new Date(e.end_datetime), now));
const completedTodos = todayTodos.length === 0;
if (remainingEvents.length === 0 && completedTodos) {
parts.push('The rest of your afternoon is clear.');
} else {
if (remainingEvents.length > 0) {
parts.push(
`${remainingEvents.length} event${remainingEvents.length > 1 ? 's' : ''} remaining this afternoon.`
);
}
if (todayTodos.length > 0) {
parts.push(`${todayTodos.length} task${todayTodos.length > 1 ? 's' : ''} still due today.`);
}
}
}
// Evening (5PM9PM)
else {
const eveningEvents = todayEvents.filter((e) => e.end_datetime && isAfter(new Date(e.end_datetime), now));
if (eveningEvents.length === 0 && tomorrowItems.length === 0) {
parts.push('Nothing left tonight, and tomorrow is clear too.');
} else {
if (eveningEvents.length > 0) {
parts.push(`${eveningEvents.length} event${eveningEvents.length > 1 ? 's' : ''} left this evening.`);
}
if (tomorrowItems.length > 0) {
parts.push(`Tomorrow has ${tomorrowItems.length} item${tomorrowItems.length > 1 ? 's' : ''} ahead.`);
}
}
}
// Reminder callout
if (activeReminders.length > 0) {
const nextReminder = activeReminders[0];
const remindTime = format(new Date(nextReminder.remind_at), 'h:mm a');
parts.push(`Don't forget: ${nextReminder.title} at ${remindTime}.`);
}
// Weather rain warning
if (weatherData && weatherData.rain_chance > 40) {
if (hour >= 5 && hour < 12) {
parts.push(`There's a ${weatherData.rain_chance}% chance of rain today — might want to grab an umbrella.`);
} else if (hour >= 12 && hour < 17) {
parts.push("Heads up, rain's looking likely this afternoon.");
} else {
parts.push(`Forecasts show ${weatherData.rain_chance}% chance of rain tomorrow, you might want to plan ahead.`);
}
}
return parts.join(' ');
}, [upcomingItems, dashboardData, weatherData]);
if (!briefing) return null;
return (
<div className="rounded-lg bg-accent/[0.04] border border-accent/10 px-4 py-3 flex items-start gap-3 animate-fade-in">
<Sparkles className="h-4 w-4 text-accent shrink-0 mt-0.5" />
<p className="text-sm text-muted-foreground italic">
{briefing}
</p>
</div>
);
}