From 263de73c548968f6d0b7291493cdc4e7f62bc0de Mon Sep 17 00:00:00 2001 From: Yashvasin Sana Date: Sat, 31 Jan 2026 20:59:08 -0500 Subject: [PATCH 1/5] Automated Calendar Events --- package-lock.json | 21 ++ package.json | 2 + src/app/api/calendar/route.ts | 51 +++++ src/app/events/page.tsx | 41 +--- src/app/layout.tsx | 12 +- src/components/events/EventList.tsx | 312 ++++++++++++++++++++++++++++ 6 files changed, 398 insertions(+), 41 deletions(-) create mode 100644 src/app/api/calendar/route.ts create mode 100644 src/components/events/EventList.tsx diff --git a/package-lock.json b/package-lock.json index 2e295eb..a86c2e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,8 @@ "name": "bits-website", "version": "0.1.0", "dependencies": { + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "lucide-react": "^0.554.0", "next": "16.0.3", "postcss": "^8.5.6", @@ -2739,6 +2741,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, + "node_modules/date-fns-tz": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/date-fns-tz/-/date-fns-tz-3.2.0.tgz", + "integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==", + "license": "MIT", + "peerDependencies": { + "date-fns": "^3.0.0 || ^4.0.0" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/package.json b/package.json index 4f03491..39ae331 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,8 @@ "lint": "eslint" }, "dependencies": { + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", "lucide-react": "^0.554.0", "next": "16.0.3", "postcss": "^8.5.6", diff --git a/src/app/api/calendar/route.ts b/src/app/api/calendar/route.ts new file mode 100644 index 0000000..fd4c746 --- /dev/null +++ b/src/app/api/calendar/route.ts @@ -0,0 +1,51 @@ +// app/api/calendar/route.ts +import { NextResponse } from "next/server"; + +type CalendarEvent = { + id: string; + summary?: string; + location?: string; + description?: string; + start: { + dateTime?: string; + date?: string; + }; +}; + +export async function GET() { + try { + const key = process.env.GOOGLE_API_KEY; + const calendarId = process.env.GOOGLE_CALENDAR_ID; + + if (!key || !calendarId) { + return NextResponse.json( + { error: "Missing GOOGLE_API_KEY or GOOGLE_CALENDAR_ID" }, + { status: 500 }, + ); + } + + const url = + `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent( + calendarId, + )}/events` + + `?key=${key}` + + `&singleEvents=true` + + `&orderBy=startTime` + + `&timeMin=${new Date().toISOString()}`; + + const res = await fetch(url); + + if (!res.ok) { + const text = await res.text(); + return NextResponse.json({ error: text }, { status: res.status }); + } + + const data: { items: CalendarEvent[] } = await res.json(); + + return NextResponse.json(data.items); + } catch (err: unknown) { + // Narrow unknown to string safely + const message = err instanceof Error ? err.message : "Unknown server error"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/events/page.tsx b/src/app/events/page.tsx index d5b0f94..c85646f 100644 --- a/src/app/events/page.tsx +++ b/src/app/events/page.tsx @@ -1,43 +1,14 @@ import Navbar from "@/components/Navbar"; -import { Calendar, Clock, MapPin } from "lucide-react"; +import EventList from "@/components/events/EventList"; -const events = [ - { date: "NOV 23", title: "General Body Meeting", time: "6:00 PM", location: "McBryde 100" }, - { date: "DEC 01", title: "Project Presentations", time: "5:30 PM", location: "Goodwin Hall" }, - { date: "DEC 05", title: "End of Semester Social", time: "7:00 PM", location: "TBD" }, -]; - -export default function Events() { +export default function EventsPage() { return ( <> - -
-
-
-
- -
-

Upcoming Events

- -
- {events.map((event, i) => ( -
-
- {event.date.split(' ')[0]} - {event.date.split(' ')[1]} -
-
-

{event.title}

-
- {event.time} - {event.location} -
-
-
- ))} -
+
+

Events

+
); -} \ No newline at end of file +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b65e258..a798e95 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -9,7 +9,8 @@ const inter = Inter({ export const metadata: Metadata = { title: "BITS | Building Impactful Tech with Students", - description: "Professional-grade software solutions built by Virginia Tech engineering talent.", + description: + "Professional-grade software solutions built by Virginia Tech engineering talent.", }; export default function RootLayout({ @@ -20,15 +21,14 @@ export default function RootLayout({ return ( {/* We apply the font variable here so Tailwind can see it */} - +
- {/* --- BACKGROUND LAYERS --- */}
{/* 1. Grain Texture (White noise on black) */}
- + {/* 2. The Tech Grid (Inverted to faint white lines) */}
@@ -38,9 +38,9 @@ export default function RootLayout({
- {children} + {children}
); -} \ No newline at end of file +} diff --git a/src/components/events/EventList.tsx b/src/components/events/EventList.tsx new file mode 100644 index 0000000..fa0c470 --- /dev/null +++ b/src/components/events/EventList.tsx @@ -0,0 +1,312 @@ +"use client"; +import { useEffect, useState } from "react"; +import { MapPin } from "lucide-react"; + +/** + * ============================================================================ + * TYPES + * ============================================================================ + */ + +/** + * Raw event structure from Google Calendar API + * Supports both timed events (dateTime) and all-day events (date) + */ +type CalendarEvent = { + id: string; + summary?: string; + location?: string; + description?: string; + start: { + dateTime?: string; // ISO 8601 format (e.g., "2026-02-10T17:00:00-05:00") + date?: string; // YYYY-MM-DD format (e.g., "2026-02-16") + }; + end?: { + dateTime?: string; + date?: string; + }; +}; + +/** + * Normalized event with parsed dates and computed properties + * Used internally after processing from raw API response + */ +type NormalizedEvent = Omit & { + startDate: Date; // Parsed JavaScript Date object + endDate?: Date; // Optional end date for multi-day events + isMultiDay: boolean; // True if event spans multiple days + start: CalendarEvent["start"]; + end?: CalendarEvent["end"]; +}; + +/** + * ============================================================================ + * UTILITY FUNCTIONS + * ============================================================================ + */ + +/** + * Parse a date string from Google Calendar API + * Handles both ISO 8601 (timed) and YYYY-MM-DD (all-day) formats + * + * @param value - Date string from API, or undefined + * @returns Parsed Date object, or undefined if parsing fails + */ +function parseGoogleDate(value?: string): Date | undefined { + if (!value) return undefined; + return new Date(value); +} + +/** + * Check if two dates fall on the same calendar day + * Ignores time component, only compares year/month/day + * + * @param date1 - First date to compare + * @param date2 - Second date to compare + * @returns True if dates are the same day + */ +function isSameDay(date1: Date, date2: Date): boolean { + return ( + date1.getFullYear() === date2.getFullYear() && + date1.getMonth() === date2.getMonth() && + date1.getDate() === date2.getDate() + ); +} + +/** + * Format a time string from a Date object + * Example: "5:00 PM" or "2:30 PM" + * + * @param date - Date object to format + * @returns Formatted time string + */ +function formatTime(date: Date): string { + return date.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + hour12: true, + }); +} + +/** + * Format a date range for display + * Single-day events show as "Mon, Jan 10" + * Multi-day events show as "Mon, Jan 10 → Wed, Jan 12" + * + * @param start - Start date + * @param end - Optional end date (if not provided or same day, shows single date) + * @returns Formatted date range string + */ +function formatDateRange(start: Date, end?: Date): string { + const startStr = start.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); + + // Single-day event or no end date provided + if (!end || isSameDay(start, end)) { + return startStr; + } + + // Multi-day event - show range + const endStr = end.toLocaleDateString("en-US", { + weekday: "short", + month: "short", + day: "numeric", + }); + + return `${startStr} → ${endStr}`; +} + +/** + * ============================================================================ + * COMPONENT: EventList + * ============================================================================ + * + * Displays upcoming events from Google Calendar + * Features: + * - Fetches events from /api/calendar endpoint + * - Shows only future events (upcoming) + * - Handles multi-day events with date ranges + * - Responsive design optimized for dark theme + * - Loading and error states + */ +export default function EventList() { + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + /** + * Fetch and process calendar events + * Runs once on component mount + */ + useEffect(() => { + fetch("/api/calendar") + .then((res) => { + if (!res.ok) throw new Error("Failed to load events"); + return res.json(); + }) + .then((data: CalendarEvent[]) => { + const now = new Date(); + const normalized: NormalizedEvent[] = []; + + // Parse and normalize events + for (const event of data) { + // Parse start date (required field) + const startDate = parseGoogleDate( + event.start.dateTime ?? event.start.date, + ); + if (!startDate) continue; + + // Skip events in the past + if (startDate < now) continue; + + // Parse end date (optional field) + const endDate = parseGoogleDate( + event.end?.dateTime ?? event.end?.date, + ); + + // Determine if event spans multiple calendar days + const isMultiDay = endDate ? !isSameDay(startDate, endDate) : false; + + normalized.push({ + ...event, + startDate, + endDate, + isMultiDay, + }); + } + + // Sort chronologically by start date (earliest first) + normalized.sort( + (a, b) => a.startDate.getTime() - b.startDate.getTime(), + ); + + setEvents(normalized); + }) + .catch((err) => { + console.error(err); + setError(err instanceof Error ? err.message : "Unknown error"); + }) + .finally(() => setLoading(false)); + }, []); + + /** + * Loading state + */ + if (loading) { + return ( +
+
+
+
+
+
+ ); + } + + /** + * Error state + */ + if (error) { + return ( +
+
+ {error} +
+
+ ); + } + + /** + * Empty state (no future events) + */ + if (!events.length) { + return ( +
+

No upcoming events

+
+ ); + } + + /** + * Main render - event cards + */ + return ( +
+
+ {/* Header */} +
+

Events

+

+ {events.length} upcoming event{events.length !== 1 ? "s" : ""} +

+
+ + {/* Event cards */} +
+ {events.map((event) => { + const dateStr = formatDateRange(event.startDate, event.endDate); + + return ( +
+
+ {/* Date Section */} +
+

+ {event.isMultiDay ? "Date Range" : "Date"} +

+

+ {dateStr} +

+ {event.startDate.getHours() !== 0 || + event.startDate.getMinutes() !== 0 ? ( +

+ {formatTime(event.startDate)} + {event.endDate && + (event.endDate.getHours() !== 0 || + event.endDate.getMinutes() !== 0) ? ( + <> – {formatTime(event.endDate)} + ) : null} +

+ ) : null} +
+ + {/* Divider (visible on desktop only) */} +
+ + {/* Content Section */} +
+ {/* Event title */} +

+ {event.summary ?? "Untitled Event"} +

+ + {/* Location (if present) */} + {event.location && ( +

+ + {event.location} +

+ )} + + {/* Description (if present, truncated to 2 lines) */} + {event.description && ( +

+ {event.description} +

+ )} +
+
+
+ ); + })} +
+
+
+ ); +} From 28e48e41615f036aaaa522d9388036a55c806900 Mon Sep 17 00:00:00 2001 From: Yashvasin Sana Date: Sat, 31 Jan 2026 21:06:09 -0500 Subject: [PATCH 2/5] Simplified Normalized Events, deleted some stuff --- src/components/events/EventList.tsx | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/components/events/EventList.tsx b/src/components/events/EventList.tsx index fa0c470..8b4691e 100644 --- a/src/components/events/EventList.tsx +++ b/src/components/events/EventList.tsx @@ -9,17 +9,16 @@ import { MapPin } from "lucide-react"; */ /** - * Raw event structure from Google Calendar API - * Supports both timed events (dateTime) and all-day events (date) + * API event structure from Google Calendar */ -type CalendarEvent = { +type ApiEvent = { id: string; summary?: string; location?: string; description?: string; start: { - dateTime?: string; // ISO 8601 format (e.g., "2026-02-10T17:00:00-05:00") - date?: string; // YYYY-MM-DD format (e.g., "2026-02-16") + dateTime?: string; + date?: string; }; end?: { dateTime?: string; @@ -31,12 +30,14 @@ type CalendarEvent = { * Normalized event with parsed dates and computed properties * Used internally after processing from raw API response */ -type NormalizedEvent = Omit & { - startDate: Date; // Parsed JavaScript Date object - endDate?: Date; // Optional end date for multi-day events - isMultiDay: boolean; // True if event spans multiple days - start: CalendarEvent["start"]; - end?: CalendarEvent["end"]; +type NormalizedEvent = { + id: string; + summary?: string; + location?: string; + description?: string; + startDate: Date; + endDate?: Date; + isMultiDay: boolean; }; /** @@ -147,7 +148,7 @@ export default function EventList() { if (!res.ok) throw new Error("Failed to load events"); return res.json(); }) - .then((data: CalendarEvent[]) => { + .then((data: ApiEvent[]) => { const now = new Date(); const normalized: NormalizedEvent[] = []; From c387a5c5f965692452a34c39d54e16e77738f687 Mon Sep 17 00:00:00 2001 From: Yashvasin Sana Date: Sat, 31 Jan 2026 21:20:59 -0500 Subject: [PATCH 3/5] fixed date ranges --- src/components/events/EventList.tsx | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/components/events/EventList.tsx b/src/components/events/EventList.tsx index 8b4691e..a8d2646 100644 --- a/src/components/events/EventList.tsx +++ b/src/components/events/EventList.tsx @@ -38,6 +38,7 @@ type NormalizedEvent = { startDate: Date; endDate?: Date; isMultiDay: boolean; + isAllDay: boolean; }; /** @@ -154,6 +155,9 @@ export default function EventList() { // Parse and normalize events for (const event of data) { + // Detect if event is all-day (has 'date' field instead of 'dateTime') + const isAllDay = !event.start.dateTime && !!event.start.date; + // Parse start date (required field) const startDate = parseGoogleDate( event.start.dateTime ?? event.start.date, @@ -164,10 +168,17 @@ export default function EventList() { if (startDate < now) continue; // Parse end date (optional field) - const endDate = parseGoogleDate( + const endDateRaw = parseGoogleDate( event.end?.dateTime ?? event.end?.date, ); + // For all-day events, Google Calendar returns end date as exclusive (next day) + // So we need to subtract one day to get the actual event end date + const endDate = + isAllDay && endDateRaw + ? new Date(endDateRaw.getTime() - 86400000) + : endDateRaw; + // Determine if event spans multiple calendar days const isMultiDay = endDate ? !isSameDay(startDate, endDate) : false; @@ -176,6 +187,7 @@ export default function EventList() { startDate, endDate, isMultiDay, + isAllDay, }); } @@ -264,8 +276,9 @@ export default function EventList() {

{dateStr}

- {event.startDate.getHours() !== 0 || - event.startDate.getMinutes() !== 0 ? ( + {!event.isAllDay && + (event.startDate.getHours() !== 0 || + event.startDate.getMinutes() !== 0) ? (

{formatTime(event.startDate)} {event.endDate && From 35c0a365d5432bc8b9e98d416f7e158645af0ebe Mon Sep 17 00:00:00 2001 From: Yashvasin Sana Date: Sat, 31 Jan 2026 22:00:49 -0500 Subject: [PATCH 4/5] upcoming and live now event stages --- src/components/events/EventList.tsx | 52 ++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 4 deletions(-) diff --git a/src/components/events/EventList.tsx b/src/components/events/EventList.tsx index a8d2646..389d5a6 100644 --- a/src/components/events/EventList.tsx +++ b/src/components/events/EventList.tsx @@ -39,6 +39,8 @@ type NormalizedEvent = { endDate?: Date; isMultiDay: boolean; isAllDay: boolean; + isToday: boolean; + isCurrentTime: boolean; }; /** @@ -164,9 +166,6 @@ export default function EventList() { ); if (!startDate) continue; - // Skip events in the past - if (startDate < now) continue; - // Parse end date (optional field) const endDateRaw = parseGoogleDate( event.end?.dateTime ?? event.end?.date, @@ -179,15 +178,31 @@ export default function EventList() { ? new Date(endDateRaw.getTime() - 86400000) : endDateRaw; + // Skip events that have already ended + const eventEndTime = endDate || startDate; + if (eventEndTime < now) continue; + // Determine if event spans multiple calendar days const isMultiDay = endDate ? !isSameDay(startDate, endDate) : false; + // Check if event is happening today + const isToday = isSameDay(startDate, now); + + // Check if event is happening at the current time (for timed events or all-day events) + // For all-day events, show as live for the entire day + // For timed events, show as live if current time is between start and end + const isCurrentTime = + isToday && + (isAllDay || (startDate <= now && (!endDate || endDate >= now))); + normalized.push({ ...event, startDate, endDate, isMultiDay, isAllDay, + isToday, + isCurrentTime, }); } @@ -265,8 +280,37 @@ export default function EventList() { return (

+ {event.isCurrentTime && ( +
+ + + Live Now! + +
+ )} + {event.isAllDay && event.isToday && !event.isCurrentTime && ( +
+ + + Live Now! + +
+ )} + {event.isToday && !event.isCurrentTime && !event.isAllDay && ( +
+ + Starting Soon! + +
+ )}
{/* Date Section */}
From 7b8e8585097447410708fe0f0f9866c08b4f91f9 Mon Sep 17 00:00:00 2001 From: Silverscrypt Date: Thu, 5 Feb 2026 13:34:34 -0500 Subject: [PATCH 5/5] Pitt wiki trauma documentation response. Gotta make everything easy to understandgit add .git add .! --- src/app/api/calendar/route.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/app/api/calendar/route.ts b/src/app/api/calendar/route.ts index fd4c746..e1756b2 100644 --- a/src/app/api/calendar/route.ts +++ b/src/app/api/calendar/route.ts @@ -1,6 +1,7 @@ // app/api/calendar/route.ts import { NextResponse } from "next/server"; +// Define TypeScript type for calender events based on API response structure type CalendarEvent = { id: string; summary?: string; @@ -12,11 +13,13 @@ type CalendarEvent = { }; }; +// Fetch upcoming events from Google Calendar API export async function GET() { try { const key = process.env.GOOGLE_API_KEY; const calendarId = process.env.GOOGLE_CALENDAR_ID; + // If either key is missing, return an error response if (!key || !calendarId) { return NextResponse.json( { error: "Missing GOOGLE_API_KEY or GOOGLE_CALENDAR_ID" }, @@ -24,6 +27,7 @@ export async function GET() { ); } + //Construct Google Calender API URL with query params const url = `https://www.googleapis.com/calendar/v3/calendars/${encodeURIComponent( calendarId, @@ -33,18 +37,21 @@ export async function GET() { `&orderBy=startTime` + `&timeMin=${new Date().toISOString()}`; + //Fetch events from Google Calender API const res = await fetch(url); + // Return error if API call fails if (!res.ok) { const text = await res.text(); return NextResponse.json({ error: text }, { status: res.status }); } + //Parse response; return events as JSON const data: { items: CalendarEvent[] } = await res.json(); return NextResponse.json(data.items); } catch (err: unknown) { - // Narrow unknown to string safely + // Handle unexpected errors; return JSON error response const message = err instanceof Error ? err.message : "Unknown server error"; return NextResponse.json({ error: message }, { status: 500 }); }