Group events into five temporal sections with section headers, date subheaders, and context-aware time display (clock time for upcoming, relative for past). Includes new useEventGrouping composable, SectionHeader and DateSubheader components, full unit and E2E test coverage. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
150 lines
3.9 KiB
TypeScript
150 lines
3.9 KiB
TypeScript
import type { StoredEvent } from './useEventStorage'
|
|
|
|
export type SectionKey = 'today' | 'thisWeek' | 'nextWeek' | 'later' | 'past'
|
|
|
|
export interface DateGroup {
|
|
dateKey: string
|
|
label: string
|
|
events: StoredEvent[]
|
|
showSubheader: boolean
|
|
}
|
|
|
|
export interface EventSection {
|
|
key: SectionKey
|
|
label: string
|
|
dateGroups: DateGroup[]
|
|
emphasized: boolean
|
|
}
|
|
|
|
const SECTION_ORDER: SectionKey[] = ['today', 'thisWeek', 'nextWeek', 'later', 'past']
|
|
|
|
const SECTION_LABELS: Record<SectionKey, string> = {
|
|
today: 'Today',
|
|
thisWeek: 'This Week',
|
|
nextWeek: 'Next Week',
|
|
later: 'Later',
|
|
past: 'Past',
|
|
}
|
|
|
|
function startOfDay(date: Date): Date {
|
|
const d = new Date(date)
|
|
d.setHours(0, 0, 0, 0)
|
|
return d
|
|
}
|
|
|
|
function endOfDay(date: Date): Date {
|
|
const d = new Date(date)
|
|
d.setHours(23, 59, 59, 999)
|
|
return d
|
|
}
|
|
|
|
function endOfWeek(date: Date): Date {
|
|
const d = new Date(date)
|
|
const dayOfWeek = d.getDay() // 0=Sun, 1=Mon, ..., 6=Sat
|
|
// ISO week: Monday is first day. End of week = Sunday.
|
|
// If today is Sunday (0), end of week is today.
|
|
// Otherwise, days until Sunday = 7 - dayOfWeek
|
|
const daysUntilSunday = dayOfWeek === 0 ? 0 : 7 - dayOfWeek
|
|
d.setDate(d.getDate() + daysUntilSunday)
|
|
return endOfDay(d)
|
|
}
|
|
|
|
function endOfNextWeek(date: Date): Date {
|
|
const thisWeekEnd = endOfWeek(date)
|
|
const d = new Date(thisWeekEnd)
|
|
d.setDate(d.getDate() + 7)
|
|
return endOfDay(d)
|
|
}
|
|
|
|
function toDateKey(date: Date): string {
|
|
const y = date.getFullYear()
|
|
const m = String(date.getMonth() + 1).padStart(2, '0')
|
|
const d = String(date.getDate()).padStart(2, '0')
|
|
return `${y}-${m}-${d}`
|
|
}
|
|
|
|
function formatDateLabel(date: Date): string {
|
|
return new Intl.DateTimeFormat(undefined, {
|
|
weekday: 'short',
|
|
day: 'numeric',
|
|
month: 'short',
|
|
}).format(date)
|
|
}
|
|
|
|
function classifyEvent(eventDate: Date, todayStart: Date, todayEnd: Date, weekEnd: Date, nextWeekEnd: Date): SectionKey {
|
|
if (eventDate < todayStart) return 'past'
|
|
if (eventDate <= todayEnd) return 'today'
|
|
if (eventDate <= weekEnd) return 'thisWeek'
|
|
if (eventDate <= nextWeekEnd) return 'nextWeek'
|
|
return 'later'
|
|
}
|
|
|
|
export function useEventGrouping(events: StoredEvent[], now: Date = new Date()): EventSection[] {
|
|
const todayStart = startOfDay(now)
|
|
const todayEnd = endOfDay(now)
|
|
const weekEnd = endOfWeek(now)
|
|
const nextWeekEnd = endOfNextWeek(now)
|
|
|
|
// Classify events into sections
|
|
const buckets: Record<SectionKey, StoredEvent[]> = {
|
|
today: [],
|
|
thisWeek: [],
|
|
nextWeek: [],
|
|
later: [],
|
|
past: [],
|
|
}
|
|
|
|
for (const event of events) {
|
|
const eventDate = new Date(event.dateTime)
|
|
const section = classifyEvent(eventDate, todayStart, todayEnd, weekEnd, nextWeekEnd)
|
|
buckets[section].push(event)
|
|
}
|
|
|
|
// Build sections
|
|
const sections: EventSection[] = []
|
|
|
|
for (const key of SECTION_ORDER) {
|
|
const sectionEvents = buckets[key]
|
|
if (sectionEvents.length === 0) continue
|
|
|
|
// Sort events
|
|
const ascending = key !== 'past'
|
|
sectionEvents.sort((a, b) => {
|
|
const diff = new Date(a.dateTime).getTime() - new Date(b.dateTime).getTime()
|
|
return ascending ? diff : -diff
|
|
})
|
|
|
|
// Group by date
|
|
const dateGroupMap = new Map<string, StoredEvent[]>()
|
|
for (const event of sectionEvents) {
|
|
const dateKey = toDateKey(new Date(event.dateTime))
|
|
const group = dateGroupMap.get(dateKey)
|
|
if (group) {
|
|
group.push(event)
|
|
} else {
|
|
dateGroupMap.set(dateKey, [event])
|
|
}
|
|
}
|
|
|
|
// Convert to DateGroup array (order preserved from sorted events)
|
|
const dateGroups: DateGroup[] = []
|
|
for (const [dateKey, groupEvents] of dateGroupMap) {
|
|
dateGroups.push({
|
|
dateKey,
|
|
label: formatDateLabel(new Date(groupEvents[0]!.dateTime)),
|
|
events: groupEvents,
|
|
showSubheader: key !== 'today',
|
|
})
|
|
}
|
|
|
|
sections.push({
|
|
key,
|
|
label: SECTION_LABELS[key],
|
|
dateGroups,
|
|
emphasized: key === 'today',
|
|
})
|
|
}
|
|
|
|
return sections
|
|
}
|