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:
parent
0b8fed2e96
commit
8320ae8789
1 changed files with 80 additions and 33 deletions
|
|
@ -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' }}>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue