- Replace DialogFooter with plain div for vertical button layout in scope dialog - Add today's remaining items to night briefing (before 5 AM) before tomorrow preview Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
154 lines
5.8 KiB
TypeScript
154 lines
5.8 KiB
TypeScript
import { useMemo } from 'react';
|
||
import { format, isSameDay, startOfDay, addDays, isAfter } from 'date-fns';
|
||
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 (9PM–5AM): 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) => 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.`);
|
||
}
|
||
}
|
||
|
||
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 (5AM–12PM)
|
||
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 (12PM–5PM)
|
||
else if (hour < 17) {
|
||
const remainingEvents = todayEvents.filter((e) => 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 (5PM–9PM)
|
||
else {
|
||
const eveningEvents = todayEvents.filter((e) => 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 (
|
||
<p className="text-sm text-muted-foreground italic px-1">
|
||
{briefing}
|
||
</p>
|
||
);
|
||
}
|