Backfill dashboard calendar summary to a minimum of 3 events

When the Mine tab has fewer than 3 relevant events in range, top it up with
the earliest not-mine events from All (Today + Upcoming combined), rendered
in italics so it's clear they aren't personally assigned.
This commit is contained in:
jtricerolph 2026-07-28 12:54:21 +00:00
parent 0b8fed2e96
commit 8320ae8789

View file

@ -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<EventSummary[]> {
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<EventSummary[]>([])
const [primary, setPrimary] = useState<EventSummary[]>([])
// 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<EventSummary[]>([])
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,6 +152,7 @@ export function CalendarSummary({ user }: { user: User }) {
{!loading && error && <p style={{ color: 'var(--text-mid)', fontSize: '0.85rem' }}>Couldn't load calendar events.</p>}
{!loading && !error && (
<>
<div style={{ display: 'grid', gap: '1.25rem', gridTemplateColumns: 'repeat(auto-fit, minmax(260px, 1fr))' }}>
<div>
<h3 style={sectionTitleStyle}>Today</h3>
@ -138,6 +175,13 @@ export function CalendarSummary({ user }: { user: User }) {
)}
</div>
</div>
{tab === 'mine' && extra.length > 0 && (
<p style={{ ...emptyStyle, fontStyle: 'italic', marginTop: '0.75rem' }}>
Italic events aren't assigned to you — shown so there's something to see.
</p>
)}
</>
)}
</div>
)
@ -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 (
<div style={{ display: 'flex', gap: '0.6rem', padding: '0.5rem 0', borderBottom: '1px solid var(--card-border)' }}>
<div style={{
display: 'flex', gap: '0.6rem', padding: '0.5rem 0', borderBottom: '1px solid var(--card-border)',
fontStyle: ev.mine ? 'normal' : 'italic',
}}>
<div style={{ width: 3, borderRadius: 2, background: ev.calendar_color, flexShrink: 0 }} />
<div style={{ minWidth: 0 }}>
<div style={{ fontSize: '0.85rem', fontWeight: 600, color: 'var(--text-dark)', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>