@asd20/ui 3.2.944 → 3.2.946
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 +1 -1
- package/src/components/atoms/Asd20CalendarEventButton/index.vue +22 -11
- package/src/components/organisms/Asd20Calendar/index.vue +152 -10
- package/src/components/templates/Asd20DetailAlternateTemplate/index.vue +50 -0
- package/src/components/templates/Asd20DetailImageFullTemplate/index.vue +50 -0
- package/src/components/templates/Asd20DetailImageTemplate/index.vue +50 -0
- package/src/components/templates/Asd20DetailTemplate/index.vue +50 -1
- package/src/data/events/district-events.json +22 -22
- package/src/helpers/expandEvents.js +67 -64
- package/src/helpers/mapEventsToDays.js +60 -58
package/package.json
CHANGED
|
@@ -6,18 +6,29 @@
|
|
|
6
6
|
target="_blank"
|
|
7
7
|
:style="{ '--accent-color': event.calendarColor }"
|
|
8
8
|
>
|
|
9
|
-
<span class="title"
|
|
10
|
-
|
|
11
|
-
>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
<span v-if="event.
|
|
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
|
|
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
|
|
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,7 +25,6 @@
|
|
|
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>
|
|
@@ -24,6 +34,7 @@
|
|
|
24
34
|
v-for="(event, index) in day.events"
|
|
25
35
|
:key="index"
|
|
26
36
|
:event="event"
|
|
37
|
+
:class="day.events.length > 1 ? 'small-dot' : ''"
|
|
27
38
|
@click.native="onEventClick(event)"
|
|
28
39
|
></Asd20CalendarEventButton>
|
|
29
40
|
</div>
|
|
@@ -67,6 +78,8 @@ export default {
|
|
|
67
78
|
month: { type: Number, default: new Date().getMonth() },
|
|
68
79
|
year: { type: Number, default: new Date().getFullYear() },
|
|
69
80
|
absolute: { type: Boolean, default: true },
|
|
81
|
+
shortWeekdays: { type: Boolean, default: false },
|
|
82
|
+
annualView: { type: Boolean, default: false },
|
|
70
83
|
},
|
|
71
84
|
|
|
72
85
|
data: () => ({
|
|
@@ -92,10 +105,18 @@ export default {
|
|
|
92
105
|
return {
|
|
93
106
|
'asd20-calendar': true,
|
|
94
107
|
'asd20-calendar--absolute': this.absolute,
|
|
108
|
+
'asd20-calendar--annual': this.annualView,
|
|
95
109
|
}
|
|
96
110
|
},
|
|
97
111
|
days() {
|
|
98
|
-
return mapEventsToDays(
|
|
112
|
+
return mapEventsToDays(
|
|
113
|
+
expandEvents(this.events, this.annualView),
|
|
114
|
+
this.year,
|
|
115
|
+
this.month,
|
|
116
|
+
)
|
|
117
|
+
},
|
|
118
|
+
weekdayNames() {
|
|
119
|
+
return ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
99
120
|
},
|
|
100
121
|
},
|
|
101
122
|
methods: {
|
|
@@ -131,6 +152,22 @@ $accent: var(--color__accent);
|
|
|
131
152
|
right: 0;
|
|
132
153
|
}
|
|
133
154
|
|
|
155
|
+
&__weekdays {
|
|
156
|
+
display: flex;
|
|
157
|
+
justify-content: space-around;
|
|
158
|
+
align-items: center;
|
|
159
|
+
flex-grow: 0;
|
|
160
|
+
flex-shrink: 0;
|
|
161
|
+
width: 100%;
|
|
162
|
+
height: space(0.875);
|
|
163
|
+
margin-bottom: space(0.125);
|
|
164
|
+
padding: 0 space(0.125);
|
|
165
|
+
background: var(--color__accent-t80);
|
|
166
|
+
color: var(--color__primary);
|
|
167
|
+
font-size: 0.875rem;
|
|
168
|
+
line-height: 1;
|
|
169
|
+
}
|
|
170
|
+
|
|
134
171
|
&__day-events {
|
|
135
172
|
flex-grow: 1;
|
|
136
173
|
&::v-deep .asd20-calendar-event-button:not(:last-of-type) {
|
|
@@ -154,7 +191,6 @@ $accent: var(--color__accent);
|
|
|
154
191
|
height: 40%;
|
|
155
192
|
width: 100%;
|
|
156
193
|
flex-shrink: 0;
|
|
157
|
-
// margin-right: space(-0.25);
|
|
158
194
|
}
|
|
159
195
|
&__day {
|
|
160
196
|
position: relative;
|
|
@@ -246,6 +282,111 @@ $accent: var(--color__accent);
|
|
|
246
282
|
}
|
|
247
283
|
}
|
|
248
284
|
}
|
|
285
|
+
&--annual {
|
|
286
|
+
.asd20-calendar__weekdays {
|
|
287
|
+
padding: 0;
|
|
288
|
+
}
|
|
289
|
+
.asd20-calendar__days {
|
|
290
|
+
position: relative;
|
|
291
|
+
list-style: none;
|
|
292
|
+
margin: 0 0 0 0;
|
|
293
|
+
padding: 0;
|
|
294
|
+
position: relative;
|
|
295
|
+
flex-grow: 0;
|
|
296
|
+
display: flex;
|
|
297
|
+
justify-content: flex-start;
|
|
298
|
+
align-items: stretch;
|
|
299
|
+
flex-wrap: wrap;
|
|
300
|
+
height: 40%;
|
|
301
|
+
width: 100%;
|
|
302
|
+
flex-shrink: 0;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.asd20-calendar__day {
|
|
306
|
+
position: relative;
|
|
307
|
+
font-size: 1rem;
|
|
308
|
+
margin-bottom: 0;
|
|
309
|
+
padding: 0;
|
|
310
|
+
width: calc(100% / 7);
|
|
311
|
+
flex-grow: 1;
|
|
312
|
+
display: flex;
|
|
313
|
+
flex-direction:column;
|
|
314
|
+
align-items: flex-start;
|
|
315
|
+
background-color: var(--website-page__alternate-background-t70);
|
|
316
|
+
min-height: 2rem;
|
|
317
|
+
outline: 1px solid var(--color__tertiary);
|
|
318
|
+
border: none;
|
|
319
|
+
box-shadow: none;
|
|
320
|
+
&.today {
|
|
321
|
+
&::v-deep .day::after {
|
|
322
|
+
content: none;
|
|
323
|
+
display: none;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
.asd20-calendar__day-events {
|
|
328
|
+
display: none !important;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
.asd20-calendar__date {
|
|
332
|
+
position: static;
|
|
333
|
+
font-size: 0.875rem;
|
|
334
|
+
margin: 0 0 0.125rem 0.125rem;
|
|
335
|
+
|
|
336
|
+
.day {
|
|
337
|
+
display: inline;
|
|
338
|
+
width: auto;
|
|
339
|
+
height: auto;
|
|
340
|
+
padding: 0;
|
|
341
|
+
font-size: 0.875rem;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.weekday,
|
|
345
|
+
.month,
|
|
346
|
+
.year {
|
|
347
|
+
display: none;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.asd20-calendar__events {
|
|
352
|
+
position: static;
|
|
353
|
+
display: flex;
|
|
354
|
+
flex-direction: row;
|
|
355
|
+
width: 100%;
|
|
356
|
+
justify-content: center;
|
|
357
|
+
|
|
358
|
+
&::v-deep .asd20-calendar-event-button {
|
|
359
|
+
background: transparent;
|
|
360
|
+
padding: 0;
|
|
361
|
+
width: space(0.375);
|
|
362
|
+
height: space(0.375);
|
|
363
|
+
// margin-right: space(0.0625);
|
|
364
|
+
.title::before {
|
|
365
|
+
width: 10px;
|
|
366
|
+
height: 10px;
|
|
367
|
+
margin: 0;
|
|
368
|
+
|
|
369
|
+
}
|
|
370
|
+
.title span {
|
|
371
|
+
display: none;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
&::v-deep .small-dot {
|
|
375
|
+
width: space(0.25);
|
|
376
|
+
height: space(0.25);
|
|
377
|
+
.title::before {
|
|
378
|
+
width: 6px;
|
|
379
|
+
height: 6px;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
.outside {
|
|
384
|
+
.asd20-calendar__date .day {
|
|
385
|
+
display: none;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
249
390
|
}
|
|
250
391
|
|
|
251
392
|
@media (max-width: 767px) {
|
|
@@ -282,10 +423,10 @@ $accent: var(--color__accent);
|
|
|
282
423
|
// min-height: calc(100% / 5);
|
|
283
424
|
|
|
284
425
|
&:nth-child(n + 1):nth-child(-n + 7) {
|
|
285
|
-
margin-top: space(1);
|
|
426
|
+
// margin-top: space(1);
|
|
286
427
|
|
|
287
|
-
box-shadow: 0 -1px 0 0 var(--color__tertiary),
|
|
288
|
-
|
|
428
|
+
// box-shadow: 0 -1px 0 0 var(--color__tertiary),
|
|
429
|
+
// 0 1px 0 0 var(--color__tertiary), -1px 0 0 0 var(--color__tertiary);
|
|
289
430
|
|
|
290
431
|
&::v-deep .asd20-calendar__date {
|
|
291
432
|
width: 100%;
|
|
@@ -309,10 +450,10 @@ $accent: var(--color__accent);
|
|
|
309
450
|
}
|
|
310
451
|
}
|
|
311
452
|
|
|
312
|
-
&:nth-child(n + 1):nth-child(1) {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
}
|
|
453
|
+
// &:nth-child(n + 1):nth-child(1) {
|
|
454
|
+
// box-shadow: 0 -1px 0 0 var(--color__tertiary),
|
|
455
|
+
// 0 1px 0 0 var(--color__tertiary);
|
|
456
|
+
// }
|
|
316
457
|
|
|
317
458
|
&.selected {
|
|
318
459
|
// outline: none;
|
|
@@ -376,4 +517,5 @@ $accent: var(--color__accent);
|
|
|
376
517
|
}
|
|
377
518
|
}
|
|
378
519
|
}
|
|
520
|
+
|
|
379
521
|
</style>
|
|
@@ -180,6 +180,56 @@ export default {
|
|
|
180
180
|
Asd20QuicklinksMenu,
|
|
181
181
|
Asd20MediaSection,
|
|
182
182
|
},
|
|
183
|
+
mounted() {
|
|
184
|
+
// Allow accordions to be injected into the DOM via the SuperEditor in the CommCenter
|
|
185
|
+
this.$nextTick(() => {
|
|
186
|
+
const accordionButtons = document.querySelectorAll('.accordion-button')
|
|
187
|
+
|
|
188
|
+
accordionButtons.forEach(button => {
|
|
189
|
+
button.addEventListener('click', () => {
|
|
190
|
+
const contentId = button.getAttribute('aria-controls')
|
|
191
|
+
const content = document.getElementById(contentId)
|
|
192
|
+
const expanded = button.getAttribute('aria-expanded') === 'true'
|
|
193
|
+
|
|
194
|
+
button.setAttribute('aria-expanded', !expanded)
|
|
195
|
+
button.querySelector('.toggle-icon').textContent = expanded
|
|
196
|
+
? '+'
|
|
197
|
+
: '-'
|
|
198
|
+
|
|
199
|
+
if (!expanded) {
|
|
200
|
+
content.style.display = 'block'
|
|
201
|
+
|
|
202
|
+
// Reset maxHeight before calculating scrollHeight (force reflow)
|
|
203
|
+
content.style.maxHeight = '0'
|
|
204
|
+
content.style.padding = '0 var(--space-1, 1em)'
|
|
205
|
+
|
|
206
|
+
requestAnimationFrame(() => {
|
|
207
|
+
content.style.transition =
|
|
208
|
+
'max-height 0.4s ease, padding 0.4s ease'
|
|
209
|
+
content.style.maxHeight = content.scrollHeight + 'px'
|
|
210
|
+
content.style.padding = 'var(--space-1, 1em)'
|
|
211
|
+
})
|
|
212
|
+
} else {
|
|
213
|
+
content.style.maxHeight = content.scrollHeight + 'px' // Set to current height for smooth transition
|
|
214
|
+
|
|
215
|
+
requestAnimationFrame(() => {
|
|
216
|
+
content.style.transition =
|
|
217
|
+
'max-height 0.4s ease, padding 0.4s ease'
|
|
218
|
+
content.style.maxHeight = '0'
|
|
219
|
+
content.style.padding = '0 var(--space-1, 1em)'
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
// Optionally hide the content after animation
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
if (content.style.maxHeight === '0px') {
|
|
225
|
+
content.style.display = 'none'
|
|
226
|
+
}
|
|
227
|
+
}, 400) // Match transition duration
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
},
|
|
183
233
|
}
|
|
184
234
|
</script>
|
|
185
235
|
|
|
@@ -172,6 +172,56 @@ export default {
|
|
|
172
172
|
Asd20QuicklinksMenu,
|
|
173
173
|
Asd20MediaSection,
|
|
174
174
|
},
|
|
175
|
+
mounted() {
|
|
176
|
+
// Allow accordions to be injected into the DOM via the SuperEditor in the CommCenter
|
|
177
|
+
this.$nextTick(() => {
|
|
178
|
+
const accordionButtons = document.querySelectorAll('.accordion-button')
|
|
179
|
+
|
|
180
|
+
accordionButtons.forEach(button => {
|
|
181
|
+
button.addEventListener('click', () => {
|
|
182
|
+
const contentId = button.getAttribute('aria-controls')
|
|
183
|
+
const content = document.getElementById(contentId)
|
|
184
|
+
const expanded = button.getAttribute('aria-expanded') === 'true'
|
|
185
|
+
|
|
186
|
+
button.setAttribute('aria-expanded', !expanded)
|
|
187
|
+
button.querySelector('.toggle-icon').textContent = expanded
|
|
188
|
+
? '+'
|
|
189
|
+
: '-'
|
|
190
|
+
|
|
191
|
+
if (!expanded) {
|
|
192
|
+
content.style.display = 'block'
|
|
193
|
+
|
|
194
|
+
// Reset maxHeight before calculating scrollHeight (force reflow)
|
|
195
|
+
content.style.maxHeight = '0'
|
|
196
|
+
content.style.padding = '0 var(--space-1, 1em)'
|
|
197
|
+
|
|
198
|
+
requestAnimationFrame(() => {
|
|
199
|
+
content.style.transition =
|
|
200
|
+
'max-height 0.4s ease, padding 0.4s ease'
|
|
201
|
+
content.style.maxHeight = content.scrollHeight + 'px'
|
|
202
|
+
content.style.padding = 'var(--space-1, 1em)'
|
|
203
|
+
})
|
|
204
|
+
} else {
|
|
205
|
+
content.style.maxHeight = content.scrollHeight + 'px' // Set to current height for smooth transition
|
|
206
|
+
|
|
207
|
+
requestAnimationFrame(() => {
|
|
208
|
+
content.style.transition =
|
|
209
|
+
'max-height 0.4s ease, padding 0.4s ease'
|
|
210
|
+
content.style.maxHeight = '0'
|
|
211
|
+
content.style.padding = '0 var(--space-1, 1em)'
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// Optionally hide the content after animation
|
|
215
|
+
setTimeout(() => {
|
|
216
|
+
if (content.style.maxHeight === '0px') {
|
|
217
|
+
content.style.display = 'none'
|
|
218
|
+
}
|
|
219
|
+
}, 400) // Match transition duration
|
|
220
|
+
}
|
|
221
|
+
})
|
|
222
|
+
})
|
|
223
|
+
})
|
|
224
|
+
},
|
|
175
225
|
}
|
|
176
226
|
</script>
|
|
177
227
|
|
|
@@ -165,6 +165,56 @@ export default {
|
|
|
165
165
|
Asd20QuicklinksMenu,
|
|
166
166
|
Asd20MediaSection,
|
|
167
167
|
},
|
|
168
|
+
mounted() {
|
|
169
|
+
// Allow accordions to be injected into the DOM via the SuperEditor in the CommCenter
|
|
170
|
+
this.$nextTick(() => {
|
|
171
|
+
const accordionButtons = document.querySelectorAll('.accordion-button')
|
|
172
|
+
|
|
173
|
+
accordionButtons.forEach(button => {
|
|
174
|
+
button.addEventListener('click', () => {
|
|
175
|
+
const contentId = button.getAttribute('aria-controls')
|
|
176
|
+
const content = document.getElementById(contentId)
|
|
177
|
+
const expanded = button.getAttribute('aria-expanded') === 'true'
|
|
178
|
+
|
|
179
|
+
button.setAttribute('aria-expanded', !expanded)
|
|
180
|
+
button.querySelector('.toggle-icon').textContent = expanded
|
|
181
|
+
? '+'
|
|
182
|
+
: '-'
|
|
183
|
+
|
|
184
|
+
if (!expanded) {
|
|
185
|
+
content.style.display = 'block'
|
|
186
|
+
|
|
187
|
+
// Reset maxHeight before calculating scrollHeight (force reflow)
|
|
188
|
+
content.style.maxHeight = '0'
|
|
189
|
+
content.style.padding = '0 var(--space-1, 1em)'
|
|
190
|
+
|
|
191
|
+
requestAnimationFrame(() => {
|
|
192
|
+
content.style.transition =
|
|
193
|
+
'max-height 0.4s ease, padding 0.4s ease'
|
|
194
|
+
content.style.maxHeight = content.scrollHeight + 'px'
|
|
195
|
+
content.style.padding = 'var(--space-1, 1em)'
|
|
196
|
+
})
|
|
197
|
+
} else {
|
|
198
|
+
content.style.maxHeight = content.scrollHeight + 'px' // Set to current height for smooth transition
|
|
199
|
+
|
|
200
|
+
requestAnimationFrame(() => {
|
|
201
|
+
content.style.transition =
|
|
202
|
+
'max-height 0.4s ease, padding 0.4s ease'
|
|
203
|
+
content.style.maxHeight = '0'
|
|
204
|
+
content.style.padding = '0 var(--space-1, 1em)'
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
// Optionally hide the content after animation
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
if (content.style.maxHeight === '0px') {
|
|
210
|
+
content.style.display = 'none'
|
|
211
|
+
}
|
|
212
|
+
}, 400) // Match transition duration
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
|
217
|
+
},
|
|
168
218
|
}
|
|
169
219
|
</script>
|
|
170
220
|
|
|
@@ -168,6 +168,56 @@ export default {
|
|
|
168
168
|
props: {
|
|
169
169
|
languageCode: { type: String, default: 'en' },
|
|
170
170
|
},
|
|
171
|
+
mounted() {
|
|
172
|
+
// Allow accordions to be injected into the DOM via the SuperEditor in the CommCenter
|
|
173
|
+
this.$nextTick(() => {
|
|
174
|
+
const accordionButtons = document.querySelectorAll('.accordion-button')
|
|
175
|
+
|
|
176
|
+
accordionButtons.forEach(button => {
|
|
177
|
+
button.addEventListener('click', () => {
|
|
178
|
+
const contentId = button.getAttribute('aria-controls')
|
|
179
|
+
const content = document.getElementById(contentId)
|
|
180
|
+
const expanded = button.getAttribute('aria-expanded') === 'true'
|
|
181
|
+
|
|
182
|
+
button.setAttribute('aria-expanded', !expanded)
|
|
183
|
+
button.querySelector('.toggle-icon').textContent = expanded
|
|
184
|
+
? '+'
|
|
185
|
+
: '-'
|
|
186
|
+
|
|
187
|
+
if (!expanded) {
|
|
188
|
+
content.style.display = 'block'
|
|
189
|
+
|
|
190
|
+
// Reset maxHeight before calculating scrollHeight (force reflow)
|
|
191
|
+
content.style.maxHeight = '0'
|
|
192
|
+
content.style.padding = '0 var(--space-1, 1em)'
|
|
193
|
+
|
|
194
|
+
requestAnimationFrame(() => {
|
|
195
|
+
content.style.transition =
|
|
196
|
+
'max-height 0.4s ease, padding 0.4s ease'
|
|
197
|
+
content.style.maxHeight = content.scrollHeight + 'px'
|
|
198
|
+
content.style.padding = 'var(--space-1, 1em)'
|
|
199
|
+
})
|
|
200
|
+
} else {
|
|
201
|
+
content.style.maxHeight = content.scrollHeight + 'px' // Set to current height for smooth transition
|
|
202
|
+
|
|
203
|
+
requestAnimationFrame(() => {
|
|
204
|
+
content.style.transition =
|
|
205
|
+
'max-height 0.4s ease, padding 0.4s ease'
|
|
206
|
+
content.style.maxHeight = '0'
|
|
207
|
+
content.style.padding = '0 var(--space-1, 1em)'
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// Optionally hide the content after animation
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
if (content.style.maxHeight === '0px') {
|
|
213
|
+
content.style.display = 'none'
|
|
214
|
+
}
|
|
215
|
+
}, 400) // Match transition duration
|
|
216
|
+
}
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
},
|
|
171
221
|
}
|
|
172
222
|
</script>
|
|
173
223
|
|
|
@@ -254,7 +304,6 @@ export default {
|
|
|
254
304
|
// max-width: 50vw;
|
|
255
305
|
}
|
|
256
306
|
}
|
|
257
|
-
|
|
258
307
|
}
|
|
259
308
|
}
|
|
260
309
|
</style>
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"summary": "Winter Break - No School",
|
|
30
30
|
"description": "No School",
|
|
31
31
|
"location": "AAHS",
|
|
32
|
-
"start": "
|
|
33
|
-
"end": "
|
|
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": "
|
|
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": "
|
|
60
|
-
"end": "
|
|
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": "
|
|
87
|
-
"end": "
|
|
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": "
|
|
114
|
-
"end": "
|
|
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": "
|
|
141
|
-
"end": "
|
|
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": "
|
|
168
|
-
"end": "
|
|
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": "
|
|
195
|
-
"end": "
|
|
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": "
|
|
222
|
-
"end": "
|
|
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": "
|
|
249
|
-
"end": "
|
|
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-
|
|
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((
|
|
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 =
|
|
27
|
-
const diffHours =
|
|
28
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|
|
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 (
|
|
42
|
-
|
|
50
|
+
if (includePastEvents || currentDay >= today) {
|
|
51
|
+
expandedEvents.push({
|
|
43
52
|
...event,
|
|
44
|
-
start:
|
|
45
|
-
end:
|
|
53
|
+
start: currentDay,
|
|
54
|
+
end: normalizedEnd,
|
|
46
55
|
originalStart: start,
|
|
47
|
-
allDay:
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
84
|
+
|
|
85
|
+
return acc.concat(expandedEvents)
|
|
82
86
|
}, [])
|
|
83
|
-
.reduce((
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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)
|
|
89
|
-
return
|
|
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 {
|
|
12
|
-
* @param {number}
|
|
13
|
-
* @param {number}
|
|
14
|
-
* @param {boolean}
|
|
15
|
-
* @returns
|
|
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
|
-
|
|
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
|
-
//
|
|
29
|
-
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
}
|