diff --git a/src/components/CalendarSummary.tsx b/src/components/CalendarSummary.tsx index b9dbe99..3a8e724 100644 --- a/src/components/CalendarSummary.tsx +++ b/src/components/CalendarSummary.tsx @@ -16,6 +16,9 @@ interface EventSummary { const RANGE_DAYS = 7 const UPCOMING_LIMIT = 6 +const MIN_TOTAL = 3 + +interface TaggedEvent extends EventSummary { mine: boolean } function toISODate(d: Date): string { const y = d.getFullYear() @@ -24,6 +27,17 @@ function toISODate(d: Date): string { return `${y}-${m}-${day}` } +function fetchEvents(scope: 'mine' | 'all'): Promise { + const url = scope === 'mine' + ? `/calendar/api/me/upcoming?days=${RANGE_DAYS}` + : `/calendar/api/events?from=${toISODate(new Date())}&to=${toISODate(new Date(Date.now() + RANGE_DAYS * 86400000))}` + return fetch(url, { credentials: 'include' }).then(r => r.ok ? r.json() : Promise.reject()) +} + +function isRelevant(ev: EventSummary, now: Date): boolean { + return occursToday(ev, now) || new Date(ev.start_at) > now +} + function occursToday(ev: EventSummary, today: Date): boolean { const start = new Date(ev.start_at) const end = new Date(ev.end_at) @@ -44,7 +58,10 @@ export function CalendarSummary({ user }: { user: User }) { const navigate = useNavigate() const calendarApp = user.apps.find(a => a.slug === 'calendar') const [tab, setTab] = useState<'mine' | 'all'>('mine') - const [events, setEvents] = useState([]) + const [primary, setPrimary] = useState([]) + // Backfilled from "all" when the Mine tab is too thin — kept separate so + // each row can be marked not-mine and rendered distinctly (see EventRow). + const [extra, setExtra] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(false) @@ -52,26 +69,45 @@ export function CalendarSummary({ user }: { user: User }) { if (!calendarApp) return setLoading(true) setError(false) - const url = tab === 'mine' - ? `/calendar/api/me/upcoming?days=${RANGE_DAYS}` - : `/calendar/api/events?from=${toISODate(new Date())}&to=${toISODate(new Date(Date.now() + RANGE_DAYS * 86400000))}` - fetch(url, { credentials: 'include' }) - .then(r => r.ok ? r.json() : Promise.reject()) - .then(setEvents) + setExtra([]) + fetchEvents(tab) + .then(setPrimary) .catch(() => setError(true)) .finally(() => setLoading(false)) }, [tab, calendarApp]) + useEffect(() => { + if (!calendarApp || tab !== 'mine' || loading || error) return + const now = new Date() + const need = MIN_TOTAL - primary.filter(ev => isRelevant(ev, now)).length + if (need <= 0) { setExtra([]); return } + + let cancelled = false + const ids = new Set(primary.map(ev => ev.id)) + fetchEvents('all') + .then(all => { + if (cancelled) return + const now2 = new Date() + setExtra(all.filter(ev => !ids.has(ev.id) && isRelevant(ev, now2)).slice(0, need)) + }) + .catch(() => {}) // backfill is best-effort — a failure here shouldn't surface as an error + return () => { cancelled = true } + }, [primary, tab, calendarApp, loading, error]) + const { today, upcoming } = useMemo(() => { const now = new Date() - const t: EventSummary[] = [] - const u: EventSummary[] = [] - for (const ev of events) { + const tagged: TaggedEvent[] = [ + ...primary.map(ev => ({ ...ev, mine: true })), + ...extra.map(ev => ({ ...ev, mine: false })), + ].sort((a, b) => new Date(a.start_at).getTime() - new Date(b.start_at).getTime()) + const t: TaggedEvent[] = [] + const u: TaggedEvent[] = [] + for (const ev of tagged) { if (occursToday(ev, now)) t.push(ev) else if (new Date(ev.start_at) > now) u.push(ev) } return { today: t, upcoming: u } - }, [events]) + }, [primary, extra]) if (!calendarApp) return null @@ -116,28 +152,36 @@ export function CalendarSummary({ user }: { user: User }) { {!loading && error &&

Couldn't load calendar events.

} {!loading && !error && ( -
-
-

Today

- {today.length === 0 - ?

Nothing on today.

- : today.map(ev => )} + <> +
+
+

Today

+ {today.length === 0 + ?

Nothing on today.

+ : today.map(ev => )} +
+ +
+

Upcoming

+ {upcoming.length === 0 + ?

Nothing else in the next {RANGE_DAYS} days.

+ : ( + <> + {upcoming.slice(0, UPCOMING_LIMIT).map(ev => )} + {upcoming.length > UPCOMING_LIMIT && ( +

+{upcoming.length - UPCOMING_LIMIT} more

+ )} + + )} +
-
-

Upcoming

- {upcoming.length === 0 - ?

Nothing else in the next {RANGE_DAYS} days.

- : ( - <> - {upcoming.slice(0, UPCOMING_LIMIT).map(ev => )} - {upcoming.length > UPCOMING_LIMIT && ( -

+{upcoming.length - UPCOMING_LIMIT} more

- )} - - )} -
-
+ {tab === 'mine' && extra.length > 0 && ( +

+ Italic events aren't assigned to you — shown so there's something to see. +

+ )} + )}
) @@ -150,9 +194,12 @@ const sectionTitleStyle: React.CSSProperties = { const emptyStyle: React.CSSProperties = { fontSize: '0.82rem', color: 'var(--text-mid)' } -function EventRow({ ev, showDay = false }: { ev: EventSummary; showDay?: boolean }) { +function EventRow({ ev, showDay = false }: { ev: TaggedEvent; showDay?: boolean }) { return ( -
+