Skip to content

Commit 88022f7

Browse files
committed
Add new calendar UI
1 parent f45f250 commit 88022f7

6 files changed

Lines changed: 408 additions & 22 deletions

File tree

_data/calendar.yml

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# Calendar configuration for the SiteCalendar component.
2+
# Fill in the values below with your own Google Calendar settings.
3+
#
4+
# - google_api_key:
5+
# Follow the steps on https://fullcalendar.io/docs/v4/google-calendar
6+
# until "Getting Set Up" to obtain the google_api_key.
7+
# - For the other 3 fields, open the calendar settings:
8+
# - Go to https://calendar.google.com
9+
# - Click the 3 dots next to the calendar name in the left side-bar
10+
# - Click "Settings and sharing"
11+
# - google_calendar_id:
12+
# - Scroll to "Integrate calendar"
13+
# - Copy "Calendar ID"
14+
# - Looks like: c_c12345@group.calendar.google.com
15+
# - google_calendar_embed_link:
16+
# - Scroll to "Integrate calendar"
17+
# - Copy "Public URL to this calendar"
18+
# - google_calendar_add_link:
19+
# - Scroll to "Access permissions for events"
20+
# - Click "Get shareable link"
21+
# - Copy the URL with calendar/u/0?cid= in it
22+
23+
google_api_key: "AIzaSyC6Fwac9uCMigu-mw-fV_LBszNtr8oviN4"
24+
google_calendar_id: ""
25+
google_calendar_embed_link: "https://calendar.google.com/calendar/embed?mode=WEEK&src=c_eeb5d06403421c5d1d8e1e484b594f734cc9e72997e5721f646e94277461f0b5@group.calendar.google.com&color=%230b8043&src=c_66a830be30ac0268f904f99250107b2fefc9b43d2545970942710b8b0024c111@group.calendar.google.com&color=%238e24aa&src=c_8e8d6006467fcf5cf80afeea70fc068b4057b0f30abb1a7a1498ec5b7aeaccfa@group.calendar.google.com&color=%231565c0&src=c_b7b05fd9aa96715177431cb68a277dab513131dfc949a61a09f156cb18554695@group.calendar.google.com&color=%23039be5&src=c_885bff102a030a09271eeef610f7b396899c384d223b43d989ed3a88d770cc13@group.calendar.google.com&color=%23d50000&src=c_3bd29413f3b0b22247139e1c51759eb5270a9e479ec651b11c2f3a52b1211cd6@group.calendar.google.com&color=%23f4511e&src=c_f5e70fbbf48d66c58b0be549181799f5619563c1c256bd0ee33410bc688d0e67@group.calendar.google.com&color=%23d81b60&ctz=America/Los_Angeles"
26+
google_calendar_add_link: ""
27+
28+
# Optional: use multiple calendars instead of google_calendar_id.
29+
# If set, the calendar will load all IDs listed below.
30+
calendars:
31+
- name: "Lecture"
32+
id: "c_eeb5d06403421c5d1d8e1e484b594f734cc9e72997e5721f646e94277461f0b5@group.calendar.google.com"
33+
- name: "Exam"
34+
id: "c_66a830be30ac0268f904f99250107b2fefc9b43d2545970942710b8b0024c111@group.calendar.google.com"
35+
- name: "Discussion"
36+
id: "c_8e8d6006467fcf5cf80afeea70fc068b4057b0f30abb1a7a1498ec5b7aeaccfa@group.calendar.google.com"
37+
- name: "Online Discussion"
38+
id: "c_b7b05fd9aa96715177431cb68a277dab513131dfc949a61a09f156cb18554695@group.calendar.google.com"
39+
- name: "Office Hours"
40+
id: "c_885bff102a030a09271eeef610f7b396899c384d223b43d989ed3a88d770cc13@group.calendar.google.com"
41+
- name: "Online Office Hours"
42+
id: "c_3bd29413f3b0b22247139e1c51759eb5270a9e479ec651b11c2f3a52b1211cd6@group.calendar.google.com"
43+
- name: "Homework Party"
44+
id: "c_f5e70fbbf48d66c58b0be549181799f5619563c1c256bd0ee33410bc688d0e67@group.calendar.google.com"
45+
46+
# If your event titles all share a prefix (e.g., "[CS189 SP26] "),
47+
# you can remove it here so the site shows clean titles.
48+
remove_prefix: ""
49+
50+
# Categories control filtering and event colors.
51+
# Events are matched in order; the first matching category wins.
52+
categories:
53+
- name: "Lecture"
54+
match: "lecture"
55+
lightTheme:
56+
--event-background: "#D6F2E3"
57+
--event-border-color: "#0B8043"
58+
darkTheme:
59+
--event-background: "#0E4A2B"
60+
--event-border-color: "#9AD9B6"
61+
- name: "Exam"
62+
match: "exam|midterm|final|quiz"
63+
lightTheme:
64+
--event-background: "#E8D8F2"
65+
--event-border-color: "#8E24AA"
66+
darkTheme:
67+
--event-background: "#4A1E5C"
68+
--event-border-color: "#D0A9E6"
69+
- name: "Discussion"
70+
match: "online discussion|remote discussion|zoom discussion"
71+
lightTheme:
72+
--event-background: "#D9EEFB"
73+
--event-border-color: "#039BE5"
74+
darkTheme:
75+
--event-background: "#0D3B54"
76+
--event-border-color: "#8DD0F3"
77+
- name: "Discussion"
78+
match: "discussion"
79+
lightTheme:
80+
--event-background: "#D6E6F7"
81+
--event-border-color: "#1565C0"
82+
darkTheme:
83+
--event-background: "#133A66"
84+
--event-border-color: "#9EC2EA"
85+
- name: "Office Hours"
86+
match: "online office hours|remote office hours|zoom office hours"
87+
lightTheme:
88+
--event-background: "#FADFD6"
89+
--event-border-color: "#F4511E"
90+
darkTheme:
91+
--event-background: "#5A2B1A"
92+
--event-border-color: "#F6B59A"
93+
- name: "Office Hours"
94+
match: "office hours"
95+
lightTheme:
96+
--event-background: "#F6D4D4"
97+
--event-border-color: "#D50000"
98+
darkTheme:
99+
--event-background: "#4D1A1A"
100+
--event-border-color: "#F3A0A0"
101+
- name: "Homework Party"
102+
match: "(?=.*homework)(?=.*party)|hw party"
103+
lightTheme:
104+
--event-background: "#F4D6E6"
105+
--event-border-color: "#D81B60"
106+
darkTheme:
107+
--event-background: "#4F1A33"
108+
--event-border-color: "#F0A6C4"

_includes/calendar.html

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
<script>
2+
// Make the calendar globally acecssible so it can be
3+
// updated with the theme switcher. If this is gross,
4+
// it's possible to just listen for changes to the root
5+
// here.
6+
var calendar;
7+
8+
let category_cnt = 0;
9+
const CALENDAR_CATEGORIES = {{ site.data.calendar.categories | jsonify }}.map((category) => {
10+
return { id: String(category_cnt++), ...category };
11+
});
12+
13+
function loadGoogleCalendar(calendar, options, cb) {
14+
const request = gapi.client.calendar.events.list({
15+
calendarId: options.calendarID,
16+
maxResults: 2500,
17+
orderBy: 'startTime',
18+
singleEvents: true,
19+
});
20+
request.execute((resp) => {
21+
const events = [];
22+
if (resp.error) {
23+
console.error(`[calendar] error loading ${options.calendarID}: ${resp.error.message}`);
24+
} else {
25+
for (const item of resp.items) {
26+
if (item.visibility === 'private') continue;
27+
28+
let title = item.summary;
29+
30+
// remove prefix (e.g. class name and sem) from every event
31+
// added by peyrin Jan 2025
32+
title = title.replace('{{ site.data.calendar.remove_prefix }}', '');
33+
34+
let category = undefined;
35+
if (!item.summary) {
36+
console.error('error with item', item);
37+
} else {
38+
for (const potentialCategory of CALENDAR_CATEGORIES) {
39+
if (potentialCategory.match) {
40+
let expr = new RegExp(potentialCategory.match);
41+
if (expr.test(title.toLowerCase())) {
42+
category = potentialCategory.id;
43+
break;
44+
}
45+
}
46+
}
47+
}
48+
49+
const event = {
50+
id: item.id,
51+
calendarId: options.calendarID,
52+
title,
53+
body: item.description,
54+
location: item.location ?? '',
55+
start: item.start.dateTime,
56+
end: item.end.dateTime,
57+
category,
58+
links: {
59+
left: item.htmlLink
60+
? {
61+
text: 'Open in Google Calendar',
62+
url: item.htmlLink,
63+
}
64+
: undefined,
65+
right: item.htmlLink
66+
? {
67+
text: 'Copy to your calendar',
68+
url: `https://calendar.google.com/calendar/u/0/r/eventedit/copy/${item.htmlLink.split('eid=')[1]}`,
69+
}
70+
: undefined,
71+
},
72+
};
73+
74+
if (options.addProps) {
75+
const obj = options.addProps(item, event);
76+
for (const [key, val] of Object.entries(obj)) {
77+
if (val !== undefined) {
78+
event[key] = val;
79+
}
80+
}
81+
}
82+
events.push(event);
83+
}
84+
}
85+
86+
cb(events);
87+
});
88+
}
89+
90+
/* global initCalendar */
91+
window.initCalendar = function initCalendar() {
92+
if (!document.getElementById('calendarContainer')) return;
93+
94+
const berkeleyTimeZoneName = 'America/Los_Angeles';
95+
const hourStart = 8; // in Berkeley timezone
96+
const hourEnd = 22;
97+
98+
calendar = window.SiteCalendar.Calendar('#calendarContainer', {
99+
currentMode: window.screen.width >= 760 ? 'week' : 'day',
100+
showLocal: true,
101+
defaultTimezone: berkeleyTimeZoneName,
102+
defaultTimezoneName: 'Berkeley',
103+
startTime: hourStart,
104+
endTime: hourEnd,
105+
weekends: false,
106+
colorScheme: window.getComputedStyle(document.documentElement).getPropertyValue('color-scheme'),
107+
categories: CALENDAR_CATEGORIES,
108+
});
109+
110+
const allEvents = [];
111+
112+
const googleAPIKey = '{{ site.data.calendar.google_api_key }}';
113+
{% if site.data.calendar.calendars %}
114+
const calendarInfoArr = {{ site.data.calendar.calendars | jsonify }};
115+
{% else %}
116+
const calendarInfoArr = [
117+
{
118+
id: '{{ site.data.calendar.google_calendar_id }}',
119+
name: 'Events',
120+
},
121+
];
122+
{% endif %}
123+
124+
const buttonGroups = [
125+
...new Set(CALENDAR_CATEGORIES.map((category) => category.name.toLowerCase().replaceAll(' ', '-'))),
126+
];
127+
const inactiveGroups = [];
128+
129+
let loadCount = calendarInfoArr.length;
130+
function onCalendarLoad(events) {
131+
for (const event of events) {
132+
allEvents.push(event);
133+
}
134+
loadCount -= 1;
135+
if (loadCount == 0) {
136+
calendar.addEvents(allEvents);
137+
138+
// When we're done loading, activate our filter buttons.
139+
document.querySelectorAll('#calendarControls button').forEach((elem) => {
140+
let action = elem.getAttribute('data-action');
141+
if (action != undefined) {
142+
elem.classList.add('active');
143+
}
144+
145+
elem.addEventListener('click', (e) => {
146+
e.preventDefault();
147+
148+
let group = elem.getAttribute('data-action').replace('toggle-category-', '');
149+
if (action != undefined && !inactiveGroups.includes(group)) {
150+
elem.classList.remove('active');
151+
inactiveGroups.push(group);
152+
} else {
153+
elem.classList.add('active');
154+
const idx = inactiveGroups.indexOf(group);
155+
inactiveGroups.splice(idx, 1);
156+
}
157+
158+
// Update the filter, since the only way to rerun it is to
159+
// update it.
160+
calendar.updateFilter((event) => {
161+
return (
162+
event.category == undefined ||
163+
inactiveGroups.includes(event.category.name.toLowerCase().replaceAll(' ', '-')) == false
164+
);
165+
});
166+
});
167+
});
168+
}
169+
}
170+
171+
gapi.load('client', () => {
172+
gapi.client.setApiKey(googleAPIKey);
173+
gapi.client.load('calendar', 'v3', () => {
174+
for (const { id: calendarID } of calendarInfoArr) {
175+
loadGoogleCalendar(
176+
calendar,
177+
{
178+
calendarID,
179+
},
180+
onCalendarLoad
181+
);
182+
}
183+
});
184+
});
185+
186+
const calendarTable = document.getElementById('calendarTable');
187+
if (calendarTable) {
188+
calendarTable.classList.remove('d-none');
189+
190+
for (const calendarInfo of calendarInfoArr) {
191+
const row = calendarTable.insertRow();
192+
const nameCell = row.insertCell();
193+
nameCell.innerHTML = calendarInfo.name;
194+
const linkCell = row.insertCell();
195+
const linkElem = document.createElement('a');
196+
linkElem.href = `https://calendar.google.com/calendar/embed?src=${encodeURIComponent(calendarInfo.id)}`;
197+
linkElem.innerText = 'Google Calendar';
198+
linkCell.appendChild(linkElem);
199+
}
200+
}
201+
};
202+
</script>
203+
204+
<link rel="stylesheet" href="{{ '/assets/calendar/calendar.min.css' | relative_url }}">
205+
<link rel="stylesheet" href="{{ '/assets/calendar/calendar-overrides.css' | relative_url }}">
206+
<script defer src="{{ '/assets/calendar/calendar.min.js' | relative_url }}"></script>
207+
208+
<script
209+
defer
210+
src="https://apis.google.com/js/api.js"
211+
crossorigin="anonymous"
212+
referrerpolicy="no-referrer"
213+
></script>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/* Calendar filter buttons: clear selected vs unselected states */
2+
#calendarControls .btn {
3+
background: #f7f9fc;
4+
border: 1px solid #d0d7de;
5+
border-radius: 999px;
6+
box-shadow: none;
7+
color: #0b3d91;
8+
font-weight: 600;
9+
margin: 0.25rem 0.35rem;
10+
padding: 0.35rem 0.9rem;
11+
transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
12+
}
13+
14+
#calendarControls .btn:hover {
15+
background: #eaf1ff;
16+
border-color: #7aa2e3;
17+
color: #0a2f6b;
18+
}
19+
20+
#calendarControls .btn.active {
21+
background: #0b3d91;
22+
border-color: #0b3d91;
23+
color: #ffffff;
24+
}
25+
26+
#calendarControls .btn:not(.active) {
27+
background: #f7f9fc;
28+
color: #334155;
29+
}
30+
31+
#calendarControls .btn:focus {
32+
outline: none;
33+
box-shadow: 0 0 0 3px rgba(11, 61, 145, 0.25);
34+
}
35+
36+
/* Compact wrapping for smaller screens */
37+
#calendarControls .input-group {
38+
flex-wrap: wrap;
39+
justify-content: center;
40+
}
41+
42+
/* Shortcuts layout: stack on separate lines for readability */
43+
#calendarShortcuts {
44+
display: flex;
45+
flex-direction: column;
46+
gap: 0.35rem;
47+
margin: 0.25rem 0 0.75rem;
48+
}
49+
50+
#calendarShortcuts .shortcut {
51+
display: block;
52+
}

assets/calendar/calendar.min.css

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/calendar/calendar.min.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)