[READ-ONLY] One Calendar is a privacy-first calendar web app built with Next.js. It has modern security features, including e2ee, password-protected sharing, and self-destructing share links 馃搮
calendar.xyehr.cn
nextjs
3.6 kB
141 lines
1export interface TimeCategory {
2 id: string
3 name: string
4 color: string
5 keywords: string[]
6}
7
8export interface TimeAnalytics {
9 totalEvents: number
10 totalHours: number
11 categorizedHours: Record<string, number>
12 mostProductiveDay: string
13 mostProductiveHour: number
14 averageEventDuration: number
15 activeDays: number
16 busiestCategoryId: string
17 longestEvent: {
18 title: string
19 duration: number
20 }
21}
22
23export function analyzeTimeUsage(
24 events: any[],
25 categories: TimeCategory[] = [],
26): TimeAnalytics {
27 const result: TimeAnalytics = {
28 totalEvents: events.length,
29 totalHours: 0,
30 categorizedHours: {},
31 mostProductiveDay: '',
32 mostProductiveHour: 0,
33 averageEventDuration: 0,
34 activeDays: 0,
35 busiestCategoryId: 'uncategorized',
36 longestEvent: {
37 title: '',
38 duration: 0,
39 },
40 }
41
42 categories.forEach((category) => {
43 result.categorizedHours[category.id] = 0
44 })
45 result.categorizedHours['uncategorized'] = 0
46
47 const eventsByDay: Record<string, number> = {}
48 const eventsByHour: Record<number, number> = {}
49 for (let i = 0; i < 24; i++) {
50 eventsByHour[i] = 0
51 }
52
53 events.forEach((event) => {
54 const startDate = new Date(event.startDate)
55 const endDate = new Date(event.endDate)
56 const durationMs = endDate.getTime() - startDate.getTime()
57 const normalizedDurationMs = event.isAllDay
58 ? Math.max(1, Math.ceil(durationMs / (1000 * 60 * 60 * 24))) *
59 24 *
60 60 *
61 60 *
62 1000
63 : durationMs
64 const durationHours = normalizedDurationMs / (1000 * 60 * 60)
65
66 result.totalHours += durationHours
67
68 if (durationHours > result.longestEvent.duration) {
69 result.longestEvent = {
70 title: event.title,
71 duration: durationHours,
72 }
73 }
74
75 const dayKey = startDate.toISOString().split('T')[0]
76 eventsByDay[dayKey] = (eventsByDay[dayKey] || 0) + durationHours
77
78 const hour = startDate.getHours()
79 eventsByHour[hour] = (eventsByHour[hour] || 0) + 1
80
81 if (
82 event.calendarId &&
83 categories.some((cat) => cat.id === event.calendarId)
84 ) {
85 result.categorizedHours[event.calendarId] += durationHours
86 return
87 }
88
89 let categorized = false
90 for (const category of categories) {
91 const matchesKeyword = category.keywords.some(
92 (keyword) =>
93 event.title.toLowerCase().includes(keyword.toLowerCase()) ||
94 (event.description &&
95 event.description.toLowerCase().includes(keyword.toLowerCase())),
96 )
97
98 if (matchesKeyword) {
99 result.categorizedHours[category.id] += durationHours
100 categorized = true
101 break
102 }
103 }
104
105 if (!categorized) {
106 result.categorizedHours['uncategorized'] += durationHours
107 }
108 })
109
110 let maxHours = 0
111 for (const [day, hours] of Object.entries(eventsByDay)) {
112 if (hours > maxHours) {
113 maxHours = hours
114 result.mostProductiveDay = day
115 }
116 }
117
118 let maxEvents = 0
119 for (const [hour, count] of Object.entries(eventsByHour)) {
120 if (count > maxEvents) {
121 maxEvents = count
122 result.mostProductiveHour = Number.parseInt(hour)
123 }
124 }
125
126 result.activeDays = Object.keys(eventsByDay).length
127 result.averageEventDuration =
128 result.totalEvents > 0 ? result.totalHours / result.totalEvents : 0
129
130 let busiestCategoryId = 'uncategorized'
131 let busiestHours = -1
132 for (const [categoryId, hours] of Object.entries(result.categorizedHours)) {
133 if (hours > busiestHours) {
134 busiestHours = hours
135 busiestCategoryId = categoryId
136 }
137 }
138 result.busiestCategoryId = busiestCategoryId
139
140 return result
141}