@asd20/ui 3.2.945 → 3.2.947

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "*.scss",
6
6
  "*.vue"
7
7
  ],
8
- "version": "3.2.945",
8
+ "version": "3.2.947",
9
9
  "private": false,
10
10
  "license": "MIT",
11
11
  "repository": {
@@ -6,18 +6,29 @@
6
6
  target="_blank"
7
7
  :style="{ '--accent-color': event.calendarColor }"
8
8
  >
9
- <span class="title"
10
- ><span>{{ sanitizedSummary }}</span></span
11
- >
12
- <span class="time">{{ start }}{{ end }}</span>
13
- <span class="start-time">{{ startTime }}</span>
14
- <span v-if="event.location.length > 0" class="location">
9
+ <span class="title">
10
+ <span>{{ sanitizedSummary }}</span>
11
+ </span>
12
+
13
+ <!-- Show time if NOT all-day -->
14
+ <span v-if="!event.allDay" class="time"> {{ start }}{{ end }} </span>
15
+
16
+ <!-- Show start-time if NOT all-day -->
17
+ <span v-if="!event.allDay" class="start-time">
18
+ {{ startTime }}
19
+ </span>
20
+
21
+ <!-- Show location if NOT all-day and location exists -->
22
+ <span v-if="!event.allDay && event.location.length > 0" class="location">
15
23
  {{ event.location }}
16
24
  </span>
25
+
26
+ <!-- Optional: show description if you want -->
17
27
  <span class="description">{{ event.description }}</span>
18
28
  </button>
19
29
  </template>
20
30
 
31
+
21
32
  <script>
22
33
  import { format, parse } from 'date-fns'
23
34
 
@@ -32,8 +43,7 @@ export default {
32
43
  return {
33
44
  'asd20-calendar-event-button': true,
34
45
  'asd20-calendar-event-button--large': this.large,
35
- 'asd20-calendar-event-button--all-day':
36
- this.event.allday || this.event.multiDay,
46
+ 'asd20-calendar-event-button--all-day': this.event.allDay,
37
47
  }
38
48
  },
39
49
  startTime() {
@@ -45,7 +55,7 @@ export default {
45
55
  start() {
46
56
  return format(
47
57
  parse(this.event.start, "yyyy-MM-dd'T'HH:mm:ssX", new Date()),
48
- 'h:mm aa, MMMM DD YYYY'
58
+ 'h:mm aa, MMMM dd yyyy'
49
59
  )
50
60
  },
51
61
  end() {
@@ -53,7 +63,7 @@ export default {
53
63
  ' to ' +
54
64
  format(
55
65
  parse(this.event.end, "yyyy-MM-dd'T'HH:mm:ssX", new Date()),
56
- 'h:mm aa, MMMM DD YYYY'
66
+ 'h:mm aa, MMMM dd yyyy'
57
67
  )
58
68
  )
59
69
  },
@@ -62,7 +72,6 @@ export default {
62
72
  },
63
73
  },
64
74
  methods: {
65
- // specifically for rSchool events that have extraneous school names in the summary
66
75
  sanitizeSummary(summary) {
67
76
  if (!summary) return summary
68
77
  const pattern = /vs\.[^>]*>Multiple Schools/
@@ -70,6 +79,8 @@ export default {
70
79
  },
71
80
  },
72
81
  }
82
+
83
+
73
84
  </script>
74
85
 
75
86
 
@@ -1,5 +1,16 @@
1
1
  <template>
2
2
  <div :class="classes">
3
+ <!-- Weekday header -->
4
+ <div class="asd20-calendar__weekdays">
5
+ <div
6
+ v-for="(dayName, index) in weekdayNames"
7
+ :key="index"
8
+ class="asd20-calendar__weekday"
9
+ >
10
+ {{ shortWeekdays ? dayName.charAt(0) : dayName }}
11
+ </div>
12
+ </div>
13
+ <!-- Days -->
3
14
  <ol class="asd20-calendar__days">
4
15
  <li
5
16
  class="asd20-calendar__day"
@@ -14,18 +25,23 @@
14
25
  @click="onClickDay($event, day)"
15
26
  >
16
27
  <div class="asd20-calendar__date">
17
- <span class="weekday">{{ day.weekday }}</span>
18
28
  <span class="month">{{ day.month }}</span>
19
29
  <span class="day">{{ day.number }}</span>
20
30
  <span class="year">{{ day.year }}</span>
21
31
  </div>
22
- <div class="asd20-calendar__events">
23
- <Asd20CalendarEventButton
24
- v-for="(event, index) in day.events"
25
- :key="index"
26
- :event="event"
27
- @click.native="onEventClick(event)"
28
- ></Asd20CalendarEventButton>
32
+ <div>
33
+ <div
34
+ class="asd20-calendar__events"
35
+ v-if="!annualView || !day.outsideOfCurrentMonth"
36
+ >
37
+ <Asd20CalendarEventButton
38
+ v-for="(event, index) in day.events"
39
+ :key="index"
40
+ :event="event"
41
+ :class="day.events.length > 1 ? 'small-dot' : ''"
42
+ @click.native="onEventClick(event)"
43
+ />
44
+ </div>
29
45
  </div>
30
46
  </li>
31
47
  </ol>
@@ -67,6 +83,8 @@ export default {
67
83
  month: { type: Number, default: new Date().getMonth() },
68
84
  year: { type: Number, default: new Date().getFullYear() },
69
85
  absolute: { type: Boolean, default: true },
86
+ shortWeekdays: { type: Boolean, default: false },
87
+ annualView: { type: Boolean, default: false },
70
88
  },
71
89
 
72
90
  data: () => ({
@@ -92,10 +110,18 @@ export default {
92
110
  return {
93
111
  'asd20-calendar': true,
94
112
  'asd20-calendar--absolute': this.absolute,
113
+ 'asd20-calendar--annual': this.annualView,
95
114
  }
96
115
  },
97
116
  days() {
98
- return mapEventsToDays(expandEvents(this.events), this.year, this.month)
117
+ return mapEventsToDays(
118
+ expandEvents(this.events, this.annualView),
119
+ this.year,
120
+ this.month,
121
+ )
122
+ },
123
+ weekdayNames() {
124
+ return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
99
125
  },
100
126
  },
101
127
  methods: {
@@ -131,6 +157,22 @@ $accent: var(--color__accent);
131
157
  right: 0;
132
158
  }
133
159
 
160
+ &__weekdays {
161
+ display: flex;
162
+ justify-content: space-around;
163
+ align-items: center;
164
+ flex-grow: 0;
165
+ flex-shrink: 0;
166
+ width: 100%;
167
+ height: space(0.875);
168
+ margin-bottom: space(0.125);
169
+ padding: 0 space(0.125);
170
+ background: var(--color__accent-t80);
171
+ color: var(--color__primary);
172
+ font-size: 0.875rem;
173
+ line-height: 1;
174
+ }
175
+
134
176
  &__day-events {
135
177
  flex-grow: 1;
136
178
  &::v-deep .asd20-calendar-event-button:not(:last-of-type) {
@@ -154,7 +196,6 @@ $accent: var(--color__accent);
154
196
  height: 40%;
155
197
  width: 100%;
156
198
  flex-shrink: 0;
157
- // margin-right: space(-0.25);
158
199
  }
159
200
  &__day {
160
201
  position: relative;
@@ -246,6 +287,111 @@ $accent: var(--color__accent);
246
287
  }
247
288
  }
248
289
  }
290
+ &--annual {
291
+ .asd20-calendar__weekdays {
292
+ padding: 0;
293
+ }
294
+ .asd20-calendar__days {
295
+ position: relative;
296
+ list-style: none;
297
+ margin: 0 0 0 0;
298
+ padding: 0;
299
+ position: relative;
300
+ flex-grow: 0;
301
+ display: flex;
302
+ justify-content: flex-start;
303
+ align-items: stretch;
304
+ flex-wrap: wrap;
305
+ height: 40%;
306
+ width: 100%;
307
+ flex-shrink: 0;
308
+ }
309
+
310
+ .asd20-calendar__day {
311
+ position: relative;
312
+ font-size: 1rem;
313
+ margin-bottom: 0;
314
+ padding: 0;
315
+ width: calc(100% / 7);
316
+ flex-grow: 1;
317
+ display: flex;
318
+ flex-direction:column;
319
+ align-items: flex-start;
320
+ background-color: var(--website-page__alternate-background-t70);
321
+ min-height: 2rem;
322
+ outline: 1px solid var(--color__tertiary);
323
+ border: none;
324
+ box-shadow: none;
325
+ &.today {
326
+ &::v-deep .day::after {
327
+ content: none;
328
+ display: none;
329
+ }
330
+ }
331
+ }
332
+ .asd20-calendar__day-events {
333
+ display: none !important;
334
+ }
335
+
336
+ .asd20-calendar__date {
337
+ position: static;
338
+ font-size: 0.875rem;
339
+ margin: 0 0 0.125rem 0.125rem;
340
+
341
+ .day {
342
+ display: inline;
343
+ width: auto;
344
+ height: auto;
345
+ padding: 0;
346
+ font-size: 0.875rem;
347
+ }
348
+
349
+ .weekday,
350
+ .month,
351
+ .year {
352
+ display: none;
353
+ }
354
+ }
355
+
356
+ .asd20-calendar__events {
357
+ position: static;
358
+ display: flex;
359
+ flex-direction: row;
360
+ width: 100%;
361
+ justify-content: center;
362
+
363
+ &::v-deep .asd20-calendar-event-button {
364
+ background: transparent;
365
+ padding: 0;
366
+ width: space(0.375);
367
+ height: space(0.375);
368
+ // margin-right: space(0.0625);
369
+ .title::before {
370
+ width: 10px;
371
+ height: 10px;
372
+ margin: 0;
373
+
374
+ }
375
+ .title span {
376
+ display: none;
377
+ }
378
+ }
379
+ &::v-deep .small-dot {
380
+ width: space(0.25);
381
+ height: space(0.25);
382
+ .title::before {
383
+ width: 6px;
384
+ height: 6px;
385
+ }
386
+ }
387
+ }
388
+ .outside {
389
+ .asd20-calendar__date .day {
390
+ display: none;
391
+ }
392
+ }
393
+ }
394
+
249
395
  }
250
396
 
251
397
  @media (max-width: 767px) {
@@ -282,10 +428,10 @@ $accent: var(--color__accent);
282
428
  // min-height: calc(100% / 5);
283
429
 
284
430
  &:nth-child(n + 1):nth-child(-n + 7) {
285
- margin-top: space(1);
431
+ // margin-top: space(1);
286
432
 
287
- box-shadow: 0 -1px 0 0 var(--color__tertiary),
288
- 0 1px 0 0 var(--color__tertiary), -1px 0 0 0 var(--color__tertiary);
433
+ // box-shadow: 0 -1px 0 0 var(--color__tertiary),
434
+ // 0 1px 0 0 var(--color__tertiary), -1px 0 0 0 var(--color__tertiary);
289
435
 
290
436
  &::v-deep .asd20-calendar__date {
291
437
  width: 100%;
@@ -309,10 +455,10 @@ $accent: var(--color__accent);
309
455
  }
310
456
  }
311
457
 
312
- &:nth-child(n + 1):nth-child(1) {
313
- box-shadow: 0 -1px 0 0 var(--color__tertiary),
314
- 0 1px 0 0 var(--color__tertiary);
315
- }
458
+ // &:nth-child(n + 1):nth-child(1) {
459
+ // box-shadow: 0 -1px 0 0 var(--color__tertiary),
460
+ // 0 1px 0 0 var(--color__tertiary);
461
+ // }
316
462
 
317
463
  &.selected {
318
464
  // outline: none;
@@ -376,4 +522,5 @@ $accent: var(--color__accent);
376
522
  }
377
523
  }
378
524
  }
525
+
379
526
  </style>
@@ -29,8 +29,8 @@
29
29
  "summary": "Winter Break - No School",
30
30
  "description": "No School",
31
31
  "location": "AAHS",
32
- "start": "2024-12-19T07:00:00Z",
33
- "end": "2025-01-05T07:00:00Z",
32
+ "start": "2025-12-19T07:00:00Z",
33
+ "end": "2026-01-05T07:00:00Z",
34
34
  "dtstamp": "2024-12-19T07:00:00Z",
35
35
  "class": "",
36
36
  "priority": "",
@@ -53,11 +53,11 @@
53
53
  "id": null,
54
54
  "uid": "MDQwMDAwMDA4MjAwRTAwMDc0QzVCNzEwMUE4MkUwMDgwMDAwMDAwMERFRTE5NzcxNThBQ0RBMDEwMDAwMDAwMDAwMDAwMDAwMTAwMDAwMDBFOEVBMjc5NkM0MDI3RjRGODFFNzY1QzQ0M0VGMDA0Mw==",
55
55
  "type": "event",
56
- "summary": "Winter Break - Break",
57
- "description": "",
56
+ "summary": "Choir Performance",
57
+ "description": "There will be a Choir Performance on December 25... If you can believe that.",
58
58
  "location": "AAHS",
59
- "start": "2024-12-25T17:00:00Z",
60
- "end": "2024-12-25T17:30:00Z",
59
+ "start": "2025-12-25T17:00:00Z",
60
+ "end": "2025-12-25T17:30:00Z",
61
61
  "dtstamp": "2024-12-25T17:00:00Z",
62
62
  "class": "",
63
63
  "priority": "",
@@ -83,8 +83,8 @@
83
83
  "summary": "All State Choir",
84
84
  "description": "",
85
85
  "location": "Denver Center for the Performing Arts (1101 13th St, Denver, CO 80204, United States)",
86
- "start": "2025-01-09T17:00:00Z",
87
- "end": "2025-01-11T17:30:00Z",
86
+ "start": "2026-01-09T17:00:00Z",
87
+ "end": "2026-01-11T17:30:00Z",
88
88
  "dtstamp": "2025-01-09T17:00:00Z",
89
89
  "class": "",
90
90
  "priority": "",
@@ -110,8 +110,8 @@
110
110
  "summary": "First day of second semester",
111
111
  "description": "<ul><li>In-person attendance is limited to boardroom capacity of 70 audience members per the Colorado Springs Fire Marshall.</li><li>Note, those who attend in person may be captured on camera during the live feed and recorded versions of the meeting.</li><li>The meeting link will be available the week of the meeting.</li></ul><br/>We invite public comment at our meetings.<br/><br/><ul><li>You must sign-up to speak between 7 a.m. and 1 p.m., the day of the board meeting by completing our Public <a href=\"https://forms.office.com/r/4nRw9yhyeS\" target=\"_blank\">Comment Form</a>.</li><li>You must be present and available to speak in-person at the podium when called upon.</li><li>You cannot provide virtual public comment.</li><li>If you cannot attend in-person, we invite you to submit your comments in writing via the Public <a href=\"https://forms.office.com/r/4nRw9yhyeS\" target=\"_blank\">Comment Form</a>. Written comments submitted through the Public Comment Form will not be read publicly at the board meeting, but they will be included as part of the public record in the minutes for the meeting.</li></ul>",
112
112
  "location": "AAHS",
113
- "start": "2025-01-05T07:00:00Z",
114
- "end": "2025-01-06T07:00:00Z",
113
+ "start": "2026-01-05T07:00:00Z",
114
+ "end": "2026-01-06T07:00:00Z",
115
115
  "dtstamp": "2025-01-05T07:00:00Z",
116
116
  "class": "",
117
117
  "priority": "",
@@ -137,8 +137,8 @@
137
137
  "summary": "Boys Varsity vs. CoronadoDohertyLibertyMullenPalmer RidgePine CreekPueblo WestRampartRock CanyonThunderRidge'>Multiple Schools (Home)",
138
138
  "description": "Type: Kadet Kickoff Tournament Opponent: CoronadoDohertyLibertyMullenPalmer RidgePine CreekPueblo WestRampartRock CanyonThunderRidge'>Multiple Schools Comments: Confirmed",
139
139
  "location": "AAHS Media Center",
140
- "start": "2025-01-19T08:20:00Z",
141
- "end": "2025-01-19T08:20:00Z",
140
+ "start": "2026-01-19T08:20:00Z",
141
+ "end": "2026-01-19T08:20:00Z",
142
142
  "dtstamp": "2025-01-19T20:20:00Z",
143
143
  "class": "",
144
144
  "priority": "",
@@ -164,8 +164,8 @@
164
164
  "summary": "Choice Night",
165
165
  "description": "",
166
166
  "location": "AAHS Media Center",
167
- "start": "2025-01-20T01:00:00Z",
168
- "end": "2025-01-20T03:00:00Z",
167
+ "start": "2026-01-20T01:00:00Z",
168
+ "end": "2026-01-20T03:00:00Z",
169
169
  "dtstamp": "2025-01-20T01:00:00Z",
170
170
  "class": "",
171
171
  "priority": "",
@@ -191,8 +191,8 @@
191
191
  "summary": "8th Grade Information Night",
192
192
  "description": "",
193
193
  "location": "AAHS Gyms",
194
- "start": "2025-02-10T01:00:00Z",
195
- "end": "2025-02-10T03:00:00Z",
194
+ "start": "2026-02-10T01:00:00Z",
195
+ "end": "2026-02-10T03:00:00Z",
196
196
  "dtstamp": "2025-02-10T01:00:00Z",
197
197
  "class": "",
198
198
  "priority": "",
@@ -218,8 +218,8 @@
218
218
  "summary": "Parent-teacher Conferences",
219
219
  "description": "",
220
220
  "location": "",
221
- "start": "2025-03-22T21:30:00Z",
222
- "end": "2025-03-23T01:30:00Z",
221
+ "start": "2026-03-22T21:30:00Z",
222
+ "end": "2026-03-23T01:30:00Z",
223
223
  "dtstamp": "2025-03-22T21:30:00Z",
224
224
  "class": "",
225
225
  "priority": "",
@@ -245,8 +245,8 @@
245
245
  "summary": "Parent-Teacher Conferences",
246
246
  "description": "",
247
247
  "location": "",
248
- "start": "2025-03-23T22:00:00Z",
249
- "end": "2025-03-24T01:00:00Z",
248
+ "start": "2026-03-23T22:00:00Z",
249
+ "end": "2026-03-24T01:00:00Z",
250
250
  "dtstamp": "2025-03-23T22:00:00Z",
251
251
  "class": "",
252
252
  "priority": "",
@@ -300,7 +300,7 @@
300
300
  "description": "",
301
301
  "location": "",
302
302
  "start": "2025-03-27T06:00:00Z",
303
- "end": "2025-03-28T06:00:00Z",
303
+ "end": "2025-03-31T06:00:00Z",
304
304
  "dtstamp": "2025-03-27T06:00:00Z",
305
305
  "class": "",
306
306
  "priority": "",
@@ -323,7 +323,7 @@
323
323
  "id": null,
324
324
  "uid": "MDQwMDAwMDA4MjAwRTAwMDc0QzVCNzEwMUE4MkUwMDgwMDAwMDAwMDIyQzE3RTIwRjYwQkQ5MDEwMDAwMDAwMDAwMDAwMDAwMTAwMDAwMDAzRjdENDg0MzUyQjdEQTQ1ODc1QjQwRDE2MEQ2NkEwOA==",
325
325
  "type": "event",
326
- "summary": "No school - Spring Break",
326
+ "summary": "No school - Spring Break Intermission",
327
327
  "description": "",
328
328
  "location": "",
329
329
  "start": "2025-03-28T06:00:00Z",
@@ -3,93 +3,96 @@ import format from 'date-fns/format'
3
3
  import addDays from 'date-fns/add_days'
4
4
  import isSameDay from 'date-fns/is_same_day'
5
5
 
6
- /**
7
- * Expands an array of events, creating new events for those
8
- * that span multiple days, and adding metadata.
9
- *
10
- * @export
11
- * @param {*} events
12
- * @returns An array of expanded events
13
- */
14
- export default function expandEvents(events) {
6
+ export default function expandEvents(events, includePastEvents = false) {
15
7
  return events
16
- .reduce((a, event) => {
17
- // Parse the event start and end times
8
+ .reduce((acc, event) => {
18
9
  const start = parse(event.start)
19
10
  const end = event.end ? parse(event.end) : start
20
11
 
21
- // Normalize today's date to ignore time
22
12
  const today = new Date()
23
13
  today.setHours(0, 0, 0, 0)
24
- // console.log('Today:', today)
25
14
 
26
- const diffTime = Math.abs(end.getTime() - start.getTime())
27
- const diffHours = Math.ceil(diffTime / (1000 * 60 * 60))
28
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
29
- const multiDay = !isSameDay(start, end)
30
- const allDay = !event.end || (diffHours > 23 && diffHours < 25)
31
- const endTimeUndetermined = event.end && event.end === event.start
15
+ const diffTime = end.getTime() - start.getTime()
16
+ const diffHours = diffTime / (1000 * 60 * 60)
17
+ let diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
32
18
 
33
- if (multiDay && !allDay) {
34
- // Expand multi-day events
19
+ // Safety net for zero-day events
20
+ if (diffDays === 0) diffDays = 1
21
+
22
+ const isAllDay = event.allDay || diffHours >= 23
23
+
24
+ // Normalize for date-only comparisons
25
+ const normalizedStart = new Date(start)
26
+ normalizedStart.setHours(0, 0, 0, 0)
27
+
28
+ const normalizedEnd = new Date(end)
29
+ normalizedEnd.setHours(0, 0, 0, 0)
30
+
31
+ // let isMultiDay
32
+
33
+ // if (isAllDay && diffDays === 1) {
34
+ // // All-day event lasting exactly one day → NOT multi-day
35
+ // isMultiDay = false
36
+ // } else {
37
+ // isMultiDay = !isSameDay(normalizedStart, normalizedEnd)
38
+ // }
39
+
40
+ const isMultiDay =
41
+ diffDays > 1 || !isSameDay(normalizedStart, normalizedEnd)
42
+
43
+ const expandedEvents = []
44
+
45
+ if (isMultiDay) {
46
+ // Multi-day event expansion
35
47
  for (let d = 0; d < diffDays; d++) {
36
- const newStart = addDays(start, d)
37
- // Normalize newStart to ignore time
38
- newStart.setHours(0, 0, 0, 0)
39
- // console.log('newStart:', newStart)
48
+ const currentDay = addDays(normalizedStart, d)
40
49
 
41
- if (newStart >= today) {
42
- a.push({
50
+ if (includePastEvents || currentDay >= today) {
51
+ expandedEvents.push({
43
52
  ...event,
44
- start: newStart,
45
- end: end,
53
+ start: currentDay,
54
+ end: normalizedEnd,
46
55
  originalStart: start,
47
- allDay: start.getHours() === 0,
48
- endTimeUndetermined,
49
- multiDay,
50
- weekday: format(newStart, 'dddd'),
51
- day: format(newStart, 'DD'),
52
- number: format(newStart, 'D'),
53
- month: format(newStart, 'MMMM'),
54
- year: format(newStart, 'YYYY'),
56
+ allDay: true,
57
+ multiDay: true,
58
+ weekday: format(currentDay, 'dddd'),
59
+ day: format(currentDay, 'DD'),
60
+ number: format(currentDay, 'D'),
61
+ month: format(currentDay, 'MMMM'),
62
+ year: format(currentDay, 'YYYY')
55
63
  })
56
64
  }
57
65
  }
58
66
  } else {
59
- // Normalize start date for single-day events
60
- const normalizedStart = new Date(start)
61
- normalizedStart.setHours(0, 0, 0, 0)
62
- // console.log('normailizedStart:', normalizedStart)
63
-
64
- if (normalizedStart >= today) {
65
- a.push({
67
+ // Single-day event
68
+ if (includePastEvents || normalizedStart >= today) {
69
+ expandedEvents.push({
66
70
  ...event,
67
- start: start,
71
+ start: start, // keep original start WITH time
68
72
  end: end,
69
73
  originalStart: null,
70
- allDay,
71
- endTimeUndetermined,
72
- multiDay,
73
- weekday: format(start, 'dddd'),
74
- day: format(start, 'DD'),
75
- number: format(start, 'D'),
76
- month: format(start, 'MMMM'),
77
- year: format(start, 'YYYY'),
74
+ allDay: isAllDay,
75
+ multiDay: false,
76
+ weekday: format(normalizedStart, 'dddd'),
77
+ day: format(normalizedStart, 'DD'),
78
+ number: format(normalizedStart, 'D'),
79
+ month: format(normalizedStart, 'MMMM'),
80
+ year: format(normalizedStart, 'YYYY')
78
81
  })
79
82
  }
80
83
  }
81
- return a
84
+
85
+ return acc.concat(expandedEvents)
82
86
  }, [])
83
- .reduce((a, e) => {
84
- // Remove duplicates
85
- const hasDuplicate = a.some(
86
- ae => ae.summary === e.summary && ae.start.getTime() === e.start.getTime() && ae.end.getTime() === e.end.getTime()
87
+ .reduce((acc, e) => {
88
+ const hasDuplicate = acc.some(
89
+ ae =>
90
+ ae.summary === e.summary &&
91
+ ae.start.getTime() === e.start.getTime() &&
92
+ ae.end.getTime() === e.end.getTime()
87
93
  )
88
- if (!hasDuplicate) a.push(e)
89
- return a
94
+ if (!hasDuplicate) acc.push(e)
95
+ return acc
90
96
  }, [])
91
- .sort((a, b) => {
92
- // Sort events by start date
93
- return a.start.getTime() - b.start.getTime()
94
- })
97
+ .sort((a, b) => a.start.getTime() - b.start.getTime())
95
98
  }
@@ -5,14 +5,14 @@ import isSameDay from 'date-fns/is_same_day'
5
5
  import getDaysInMonth from 'date-fns/get_days_in_month'
6
6
 
7
7
  /**
8
- * Given an array of events
8
+ * Given an array of events, returns an array of days with events grouped by date.
9
9
  *
10
10
  * @export
11
- * @param {*} events
12
- * @param {number} [year=0]
13
- * @param {number} [month=0]
14
- * @param {boolean} [showAll=false]
15
- * @returns An array days with just their events
11
+ * @param {Array} events - Array of events (should already be expanded if needed)
12
+ * @param {number} year - Target year (used when showAll is false)
13
+ * @param {number} month - Target month (zero-indexed, used when showAll is false)
14
+ * @param {boolean} showAll - If true, returns only days with events (useful for agenda views)
15
+ * @returns {Array} Array of day objects { date, events, ... }
16
16
  */
17
17
  export default function mapEventsToDays(
18
18
  events,
@@ -20,67 +20,69 @@ export default function mapEventsToDays(
20
20
  month = 0,
21
21
  showAll = false
22
22
  ) {
23
- // Array to store days
23
+ const today = new Date()
24
+ today.setHours(0, 0, 0, 0)
25
+
24
26
  let days = []
25
27
 
26
- // If we are supposed to show all days, even outside of current month (whole start-end range)
27
28
  if (showAll) {
28
- // Store dates from events
29
- let dates = events
30
- // Get unique dates
31
- .reduce((results, e) => {
32
- let d = parse(e.start)
33
- let f = format(d, 'YYYY-MM-DD')
34
- if (results.indexOf(f) === -1) {
35
- results.push(f)
36
- }
37
- return results
38
- }, [])
39
- // parse unique dates
40
- dates.map(f => parse(f))
29
+ // Collect unique event dates
30
+ const uniqueDatesMap = new Map()
41
31
 
42
- // Map dates to days, with events inserted, formatted elements
43
- days = dates.map(d => {
44
- return {
45
- outsideOfCurrentMonth: false,
46
- date: d,
47
- unix: d.valueOf(),
48
- today: isSameDay(d, parse(new Date())),
49
- events: events.filter(event => isSameDay(d, parse(event.start))),
50
- weekday: format(d, 'dddd'),
51
- day: format(d, 'DD'),
52
- shortDay: format(d, 'D'),
53
- number: format(d, 'D'),
54
- month: format(d, 'MMMM'),
55
- year: format(d, 'YYYY'),
32
+ events.forEach(event => {
33
+ const eventDate = parse(event.start)
34
+ const dateKey = format(eventDate, 'YYYY-MM-DD')
35
+ if (!uniqueDatesMap.has(dateKey)) {
36
+ uniqueDatesMap.set(dateKey, eventDate)
56
37
  }
57
38
  })
39
+
40
+ days = Array.from(uniqueDatesMap.values()).map(date => createDayObject(date, events, false, today))
58
41
  } else {
59
- let firstd = new Date(year, month, 1)
60
- let lastd = new Date(year, month + 1, 0)
61
- let daysInMonth = getDaysInMonth(firstd) // Total days in month
62
- let previousMonthDays = firstd.getDay() // Day of week of first day of month
63
- let offset = previousMonthDays * -1
64
- let nextMonthDays = offset + (6 - lastd.getDay())
65
- let total = daysInMonth + previousMonthDays + nextMonthDays
42
+ // Month view: create a full calendar grid
43
+ const firstDayOfMonth = new Date(year, month, 1)
44
+ const lastDayOfMonth = new Date(year, month + 1, 0)
45
+ const daysInMonth = getDaysInMonth(firstDayOfMonth)
46
+
47
+ const prevMonthDays = firstDayOfMonth.getDay() // How many days to show before the 1st of the month (0=Sunday)
48
+ const nextMonthDays = 6 - lastDayOfMonth.getDay() // How many days to show after the last day of the month
49
+ const totalDays = daysInMonth + prevMonthDays + nextMonthDays
66
50
 
67
- // Loop through each date
68
- for (let i = offset; i < total; i++) {
69
- let d = addDays(firstd, i)
70
- days.push({
71
- outsideOfCurrentMonth: i < 0 || i > daysInMonth - 1 ? true : false,
72
- date: d,
73
- unix: d.valueOf(),
74
- today: isSameDay(d, parse(new Date())),
75
- events: events.filter(event => isSameDay(d, parse(event.start))),
76
- weekday: format(d, 'dddd'),
77
- day: format(d, 'DD'),
78
- number: format(d, 'D'),
79
- month: format(d, 'MMMM'),
80
- year: format(d, 'YYYY'),
81
- })
51
+ for (let i = -prevMonthDays; i < totalDays - prevMonthDays; i++) {
52
+ const date = addDays(firstDayOfMonth, i)
53
+ const outsideOfCurrentMonth = i < 0 || i >= daysInMonth
54
+ days.push(createDayObject(date, events, outsideOfCurrentMonth, today))
82
55
  }
83
56
  }
84
57
 
85
- return days.sort((a, b) => (a.unix > b.unix ? 1 : b.unix > a.unix ? -1 : 0))
58
+ // Sort by unix timestamp (probably unnecessary unless something weird happens)
59
+ return days.sort((a, b) => a.unix - b.unix)
60
+ }
61
+
62
+ /**
63
+ * Builds a day object with events attached.
64
+ *
65
+ * @param {Date} date - The date this day represents.
66
+ * @param {Array} events - The full events list.
67
+ * @param {boolean} outsideOfCurrentMonth - Whether this date is outside the target month.
68
+ * @param {Date} today - Today's date (normalized to 00:00).
69
+ * @returns {Object} The day object.
70
+ */
71
+ function createDayObject(date, events, outsideOfCurrentMonth = false, today) {
72
+ const normalizedDate = new Date(date)
73
+ normalizedDate.setHours(0, 0, 0, 0)
74
+
75
+ return {
76
+ outsideOfCurrentMonth,
77
+ date: normalizedDate,
78
+ unix: normalizedDate.getTime(),
79
+ today: isSameDay(normalizedDate, today),
80
+ events: events.filter(event => isSameDay(parse(event.start), normalizedDate)),
81
+ weekday: format(normalizedDate, 'dddd'),
82
+ day: format(normalizedDate, 'DD'),
83
+ shortDay: format(normalizedDate, 'D'),
84
+ number: format(normalizedDate, 'D'),
85
+ month: format(normalizedDate, 'MMMM'),
86
+ year: format(normalizedDate, 'YYYY'),
87
+ }
86
88
  }