feat(events): Add script to fetch and display Zoho calendar events

This commit introduces a new script that fetches and normalizes event data from Zoho Calendar using OAuth2. The script makes an API call to retrieve calendar events for the current month. The event data is then processed and formatted to be used by a Nunjucks template.

Key Changes:
- test(dummy-data): Created a dummy calendar and dummy events for development and testing.
- feat(events): Added a new script to fetch event data from Zoho Calendar.
- feat(oauth2): Implemented OAuth2 authentication for secure API access.
- refactor(data): Normalized the fetched data payload for consistency.
- feat(ui): Integrated the normalized data with a Nunjucks template to dynamically populate "event cards" on the community events page.
This commit is contained in:
2025-09-05 22:22:02 -05:00
parent 873f535f25
commit af7e76f01f
21 changed files with 1644 additions and 312 deletions

View File

@@ -0,0 +1,154 @@
require('dotenv').config();
const fetch = require('node-fetch');
const fs = require('fs');
const path = require('path');
const calendarUid = process.env.ZOHO_CALENDAR_UID;
let accessToken = process.env.ZOHO_ACCESS_TOKEN;
const refreshToken = process.env.ZOHO_REFRESH_TOKEN;
const clientId = process.env.ZOHO_CLIENT_ID;
const clientSecret = process.env.ZOHO_CLIENT_SECRET;
// Dates for current month
const currentDate = new Date();
const currentYear = currentDate.getFullYear();
const currentMonth = (currentDate.getMonth() + 1).toString().padStart(2, '0');
const lastDay = new Date(currentYear, currentDate.getMonth() + 1, 0).getDate();
const startDate = `${currentYear}${currentMonth}01`;
const endDate = `${currentYear}${currentMonth}${lastDay}`;
const zohoApiUrl = `https://calendar.zoho.com/api/v1/calendars/${calendarUid}`;
async function refreshAccessToken() {
const params = new URLSearchParams();
params.append('grant_type', 'refresh_token');
params.append('client_id', clientId);
params.append('client_secret', clientSecret);
params.append('refresh_token', refreshToken);
const response = await fetch('https://accounts.zoho.com/oauth/v2/token', {
method: 'POST',
body: params
});
const data = await response.json();
if (!data.access_token) {
throw new Error('Failed to refresh access token: ' + JSON.stringify(data));
}
const envPath = path.resolve(process.cwd(), '.env');
const envContents = fs.readFileSync(envPath, 'utf-8')
.split('\n')
.map(line => line.startsWith('ZOHO_ACCESS_TOKEN=') ? `ZOHO_ACCESS_TOKEN=${data.access_token}` : line)
.join('\n');
fs.writeFileSync(envPath, envContents, 'utf-8');
accessToken = data.access_token;
}
function parseZohoTimestamp(start, isAllDay) {
if (isAllDay) {
const year = parseInt(start.slice(0, 4));
const month = parseInt(start.slice(4, 6)) - 1;
const day = parseInt(start.slice(6, 8));
return new Date(year, month, day);
} else {
const year = parseInt(start.slice(0, 4));
const month = parseInt(start.slice(4, 6)) - 1;
const day = parseInt(start.slice(6, 8));
const hour = parseInt(start.slice(9, 11));
const minute = parseInt(start.slice(11, 13));
const second = parseInt(start.slice(13, 15));
return new Date(year, month, day, hour, minute, second);
}
}
function formatEventDate(startDate, endDate, isAllDay) {
const start = parseZohoTimestamp(startDate, isAllDay);
if (isAllDay) {
return start.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
} else {
const end = parseZohoTimestamp(endDate, isAllDay);
const optionsDate = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' };
const optionsTime = { hour: 'numeric', minute: '2-digit', hour12: true };
const dateStr = start.toLocaleDateString('en-US', optionsDate);
const startTimeStr = start.toLocaleTimeString('en-US', optionsTime);
const endTimeStr = end.toLocaleTimeString('en-US', optionsTime);
return `${dateStr}${startTimeStr} ${endTimeStr}`;
}
}
module.exports = async function() {
try {
const eventsListUrl = `${zohoApiUrl}/events?range={"start":"${startDate}","end":"${endDate}"}`;
console.log(`Making initial API call to: ${eventsListUrl}`);
console.log(`Using access token`);
let response = await fetch(eventsListUrl, {
headers: { Authorization: `Zoho-oauthtoken ${accessToken}` }
});
if (response.status === 401) {
console.log("Access token is expired, refreshing...");
await refreshAccessToken();
response = await fetch(eventsListUrl, {
headers: { Authorization: `Zoho-oauthtoken ${accessToken}` }
});
}
if (!response.ok) {
const text = await response.text();
throw new Error(`API call failed: ${response.status} ${response.statusText} - ${text}`);
}
const data = await response.json();
console.log("Initial API call successful. Received data:");
console.dir(data, { depth: null });
const normalizedEventsPromises = data.events.map(async (event) => {
const basicEvent = {
calid: event.calid,
title: event.title,
uid: event.uid,
start: event.dateandtime.start,
end: event.dateandtime.end,
isallday: event.isallday,
location: event.location,
displayDate: formatEventDate(event.dateandtime.start, event.dateandtime.end, event.isallday),
};
console.log(`Processing event with UID: ${basicEvent.uid}`);
if (basicEvent.uid) {
const eventDetailsUrl = `${zohoApiUrl}/events/${basicEvent.uid}`;
console.log(`Making details API call to: ${eventDetailsUrl}`);
const detailsResponse = await fetch(eventDetailsUrl, {
headers: { Authorization: `Zoho-oauthtoken ${accessToken}` }
});
if (detailsResponse.ok) {
const detailsData = await detailsResponse.json();
console.log(`Details API call for UID ${basicEvent.uid} successful. Received data:`, detailsData);
if (detailsData.events && detailsData.events.length > 0) {
basicEvent.description = detailsData.events[0].description;
console.log(`Description added for event UID: ${basicEvent.uid}`);
}
} else {
console.warn(`Failed to fetch details for event UID: ${basicEvent.uid}`);
console.log(`Details response status: ${detailsResponse.status}`);
}
}
return basicEvent;
});
const normalizedEvents = await Promise.all(normalizedEventsPromises);
console.log("All events processed. Final normalized events array:", normalizedEvents);
return normalizedEvents;
} catch (error) {
console.error("Error fetching Zoho events:", error);
return [];
}
};