Skip to content

Commit 1f9309e

Browse files
feat: restore and expand calendar agenda
1 parent 54512a5 commit 1f9309e

3 files changed

Lines changed: 92 additions & 21 deletions

File tree

src/components/home/CalendarSection.astro

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
---
22
import { LANG, LOCALE } from '../../i18n/config'
33
import { calendarT } from '../../i18n/calendar'
4+
import { withBase } from '../../lib/base-path'
45
56
const t = calendarT[LANG]
7+
const calendarApiUrl = withBase('/api/calendar.json')
68
---
79

810
<section id='calendar' class='reveal px-2 py-20 sm:px-6' style='--reveal-delay: 90ms;'>
@@ -69,7 +71,7 @@ const t = calendarT[LANG]
6971
</div>
7072
</section>
7173

72-
<script is:inline define:vars={{ locale: LOCALE, labels: t }} lang='js'>
74+
<script is:inline define:vars={{ locale: LOCALE, labels: t, calendarApiUrl }} lang='js'>
7375
(() => {
7476
const dayEventsContainer = document.querySelector('[data-calendar-day-events]')
7577
const dayLabel = document.querySelector('[data-calendar-day-label]')
@@ -138,19 +140,39 @@ const t = calendarT[LANG]
138140
return map
139141
}
140142

143+
const hasEventsOnDate = (eventsByDay, key) => (eventsByDay.get(key) || []).length > 0
144+
141145
const getDefaultSelectedDate = (eventsByDay) => {
142-
const month = displayDate.getMonth()
143146
const year = displayDate.getFullYear()
147+
const month = displayDate.getMonth()
148+
const monthStart = new Date(year, month, 1)
149+
const mondayOffset = (monthStart.getDay() + 6) % 7
150+
const weekStart = new Date(year, month, monthStart.getDate() - mondayOffset)
151+
const weekEnd = new Date(
152+
weekStart.getFullYear(),
153+
weekStart.getMonth(),
154+
weekStart.getDate() + 6,
155+
23,
156+
59,
157+
59,
158+
999,
159+
)
160+
144161
const keys = [...eventsByDay.keys()].filter((key) => {
145162
const date = keyToDate(key)
163+
164+
if (calendarView === 'week') {
165+
return date >= weekStart && date <= weekEnd
166+
}
167+
146168
return date.getFullYear() === year && date.getMonth() === month
147169
})
148170

149171
if (keys.length > 0) {
150172
return keys.sort()[0]
151173
}
152174

153-
return getDateKey(new Date(year, month, 1))
175+
return calendarView === 'week' ? getDateKey(weekStart) : getDateKey(monthStart)
154176
}
155177

156178
const renderCalendarGrid = (events) => {
@@ -173,12 +195,8 @@ const t = calendarT[LANG]
173195
calendarLabel.textContent = monthLabelFormatter.format(baseDate)
174196
}
175197

176-
const eventDays = new Set(
177-
events
178-
.map((event) => new Date(event.start))
179-
.filter((date) => !Number.isNaN(date.getTime()))
180-
.map((date) => getDateKey(date)),
181-
)
198+
const eventsByDay = getEventsByDay(events)
199+
const eventDays = new Set(eventsByDay.keys())
182200

183201
calendarGrid.innerHTML = ''
184202

@@ -204,15 +222,22 @@ const t = calendarT[LANG]
204222

205223
if (inMonth) {
206224
const key = getDateKey(currentDate)
225+
const hasEvents = hasEventsOnDate(eventsByDay, key)
207226
dayCell.textContent = String(currentDate.getDate())
208227
dayCell.classList.remove('text-[var(--ink-soft)]')
209-
dayCell.classList.add('cursor-pointer', 'text-[var(--ink)]')
210-
211-
dayCell.addEventListener('click', () => {
212-
selectedDateKey = key
213-
renderCalendarGrid(loadedEvents)
214-
renderSelectedDayEvents(loadedEvents)
215-
})
228+
dayCell.classList.add('text-[var(--ink)]')
229+
230+
if (hasEvents) {
231+
dayCell.classList.add('cursor-pointer')
232+
dayCell.addEventListener('click', () => {
233+
selectedDateKey = key
234+
renderCalendarGrid(loadedEvents)
235+
renderSelectedDayEvents(loadedEvents)
236+
})
237+
} else {
238+
dayCell.classList.add('cursor-default')
239+
dayCell.setAttribute('aria-disabled', 'true')
240+
}
216241

217242
if (eventDays.has(key)) {
218243
dayCell.classList.add('bg-primary', 'font-semibold', 'text-white')
@@ -308,13 +333,13 @@ const t = calendarT[LANG]
308333
return `${dateLabel} - ${labels.at} ${timeLabel}`
309334
}
310335

311-
const loadCalendar = async () => {
336+
const loadCalendar = async () => {
312337
setVisible(loading, true)
313338
setVisible(error, false)
314-
setVisible(empty, false)
339+
setVisible(dayEmpty, false)
315340

316341
try {
317-
const response = await fetch('/api/calendar.json', {
342+
const response = await fetch(calendarApiUrl, {
318343
headers: {
319344
accept: 'application/json',
320345
},
@@ -382,6 +407,7 @@ const t = calendarT[LANG]
382407
item.classList.toggle('text-[var(--ink-soft)]', !selected)
383408
})
384409

410+
selectedDateKey = getDefaultSelectedDate(getEventsByDay(loadedEvents))
385411
renderCalendarGrid(loadedEvents)
386412
renderSelectedDayEvents(loadedEvents)
387413
})
@@ -395,7 +421,7 @@ const t = calendarT[LANG]
395421
} else {
396422
displayDate = new Date(displayDate.getFullYear(), displayDate.getMonth() + 1, 1)
397423
}
398-
selectedDateKey = getDateKey(new Date(displayDate.getFullYear(), displayDate.getMonth(), 1))
424+
selectedDateKey = getDefaultSelectedDate(getEventsByDay(loadedEvents))
399425
renderCalendarGrid(loadedEvents)
400426
renderSelectedDayEvents(loadedEvents)
401427
})

src/pages/api/calendar.json.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,56 @@
11
import type { APIRoute } from 'astro'
2+
import { getCollection } from 'astro:content'
23

4+
import type { CalendarEvent } from '../../lib/vigotech/calendar'
35
import { fetchGoogleCalendarEvents, getCalendarIcsUrl } from '../../lib/vigotech/calendar'
46

7+
const normalizeTitle = (value: string): string =>
8+
value
9+
.normalize('NFKD')
10+
.replace(/[\u0300-\u036f]/g, '')
11+
.toLowerCase()
12+
.replace(/\s+/g, ' ')
13+
.trim()
14+
15+
const mapContentEvent = (entry: Awaited<ReturnType<typeof getCollection<'events'>>>[number]): CalendarEvent => ({
16+
id: entry.data.sourceId,
17+
title: entry.data.title,
18+
start: entry.data.dateISO,
19+
end: null,
20+
location: entry.data.location,
21+
description: entry.data.description,
22+
htmlLink: entry.data.link,
23+
allDay: false,
24+
})
25+
26+
const mergeEventsByTitle = (contentEvents: CalendarEvent[], googleEvents: CalendarEvent[]): CalendarEvent[] => {
27+
const merged = new Map<string, CalendarEvent>()
28+
29+
for (const event of contentEvents) {
30+
merged.set(normalizeTitle(event.title), event)
31+
}
32+
33+
for (const event of googleEvents) {
34+
merged.set(normalizeTitle(event.title), event)
35+
}
36+
37+
return [...merged.values()].sort(
38+
(a, b) => new Date(a.start).getTime() - new Date(b.start).getTime(),
39+
)
40+
}
41+
542
export const GET: APIRoute = async ({ request }) => {
643
const controller = new AbortController()
744
const timeout = setTimeout(() => controller.abort(), 8000)
845

946
try {
10-
const { events, source } = await fetchGoogleCalendarEvents(controller.signal)
47+
const [{ events: googleEvents, source }, contentEntries] = await Promise.all([
48+
fetchGoogleCalendarEvents(controller.signal),
49+
getCollection('events'),
50+
])
51+
52+
const contentEvents = contentEntries.map(mapContentEvent)
53+
const events = mergeEventsByTitle(contentEvents, googleEvents)
1154

1255
return new Response(
1356
JSON.stringify({

src/pages/index.astro

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import DocumentsSection from '../components/home/DocumentsSection.astro'
55
import FriendsSection from '../components/home/FriendsSection.astro'
66
import GroupsSection from '../components/home/GroupsSection.astro'
77
import HeroSection from '../components/home/HeroSection.astro'
8+
import CalendarSection from '../components/home/CalendarSection.astro'
89
import ConversationSection from '../components/home/ConversationSection.astro'
910
import VideosSection from '../components/home/VideosSection.astro'
1011
import BaseLayout from '../layouts/BaseLayout.astro'
@@ -31,6 +32,7 @@ const recentVideos = getRecentVideos(videos, 3)
3132
>
3233
<HeroSection featuredGroups={featuredGroups} totalGroups={groups.length} upcomingEvents={upcomingEvents} />
3334
<GroupsSection groups={sortedGroups} />
35+
<CalendarSection />
3436
<VideosSection videos={recentVideos} />
3537
<ConversationSection />
3638
<DocumentsSection docs={docs} />

0 commit comments

Comments
 (0)