Kyle Pope 56876841c7 Fix scope dialog button alignment and night briefing showing wrong day
- 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>
2026-02-22 02:13:16 +08:00

154 lines
5.8 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 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) => 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 (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) => 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) => 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>
);
}