From 8810da3b476dd1666187ff522f2c2786bedf538c Mon Sep 17 00:00:00 2001 From: Atiqur Rahman Date: Wed, 2 Oct 2024 02:51:44 -0500 Subject: [PATCH 1/2] setup api for google events --- src/app/api/events/byDate/route.ts | 40 +++++++ src/app/api/events/route.ts | 50 ++++++++ src/components/Navbar.tsx | 181 ++++++++++++++--------------- src/lib/types.d.ts | 6 + 4 files changed, 184 insertions(+), 93 deletions(-) create mode 100644 src/app/api/events/byDate/route.ts create mode 100644 src/app/api/events/route.ts create mode 100644 src/lib/types.d.ts diff --git a/src/app/api/events/byDate/route.ts b/src/app/api/events/byDate/route.ts new file mode 100644 index 0000000..eb36ca4 --- /dev/null +++ b/src/app/api/events/byDate/route.ts @@ -0,0 +1,40 @@ +import { NextResponse } from "next/server"; +import { GET as getEvents } from "../route"; +import type { Event } from "@/lib/types.d.ts"; + +const calendarId = process.env.GOOGLE_CALENDAR_ID; +const endpoint = `https://www.googleapis.com/calendar/v3/calendars/${calendarId}/events`; + +export async function GET(request: Request) { + const url = new URL(request.url); + const year = parseInt(url.searchParams.get("year") || "0", 10); + const month = parseInt(url.searchParams.get("month") || "0", 10); + + // Fetch filtered events directly + const filteredEvents = await byDate(month, year); + return NextResponse.json(filteredEvents); +} + +export async function byDate(month: number, year: number): Promise { + const response = await getEvents(); // Fetch the events data directly + const data = await response.json(); // Parse the response to JSON + const earliest = new Date(year, month, 1); + const latest = new Date(year, month + 1, 0); + + // Filter and transform events in a single pass + const filteredEvents = data + .map((item: any) => { + const startDate = item.start ? new Date(item.start) : undefined; // Ensure start is defined + return { + ...item, + start: startDate, + }; + }) + .filter((event: Event) => { + if (!event.start) return false; // Exclude events with no start date + const eventDate = new Date(event.start); + return eventDate >= earliest && eventDate <= latest; + }); + + return filteredEvents; +} diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts new file mode 100644 index 0000000..a566169 --- /dev/null +++ b/src/app/api/events/route.ts @@ -0,0 +1,50 @@ +import type { Event } from "@/lib/types.d.ts"; +import { NextResponse } from "next/server"; + +export const dynamic = "force-dynamic"; +const calendarId = process.env.GOOGLE_CALENDAR_ID; +const endpoint = `https://www.googleapis.com/calendar/v3/calendars/${calendarId}/events`; + +export async function GET() { + // Fetch and parse data + const response = await fetch( + `${endpoint}?key=${process.env.CALENDAR_API_KEY}` + ); + const data = await response.json(); + + const items = data.items as unknown[] | undefined; + + if (!items || !Array.isArray(items) || items.length === 0) { + return new Response("No events found", { status: 404 }); + } + + // Use a single pass to parse and filter + const now = new Date(); + now.setHours(0, 0, 0, 0); + + const upcoming = items + .filter((item: unknown) => { + // Check for necessary properties + return ( + item && + typeof item === "object" && + "id" in item && + "summary" in item && + "start" in item && + typeof item.start === "object" && + item.start !== null && + "dateTime" in item.start && + typeof item.start.dateTime === "string" + ); + }) + .map((item: any) => ({ + id: item.id as string, + title: item.summary as string, + location: typeof item.location === "string" ? item.location : undefined, + start: new Date(item.start.dateTime), + })) + .filter((event: Event) => event.start !== undefined && event.start > now) + .sort((a, b) => a.start.getTime() - b.start.getTime()); + + return new NextResponse(JSON.stringify(upcoming)); +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index d362eb5..a988ff2 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -2,102 +2,97 @@ import React, { useState } from "react"; const Navbar = () => { - const [isMenuOpen, setIsMenuOpen] = useState(false); + const [isMenuOpen, setIsMenuOpen] = useState(false); - const toggleMenu = () => { - setIsMenuOpen(!isMenuOpen); - }; + const toggleMenu = () => { + setIsMenuOpen(!isMenuOpen); + }; - // TODO: Replace "#" with actual links - const menuItems = [ - { name: "about", link: "/about" }, - { name: "officers", link: "/officers" }, - { name: "events", link: "#" }, - { name: "connect", link: "/connect" }, - { - name: "apply", - link: "https://mavorgs.campuslabs.com/engage/organization/acm", - }, - ]; + // TODO: Replace "#" with actual links + const menuItems = [ + { name: "about", link: "/about" }, + { name: "officers", link: "/officers" }, + { name: "events", link: "/events" }, + { name: "connect", link: "/connect" }, + { + name: "apply", + link: "https://mavorgs.campuslabs.com/engage/organization/acm", + }, + ]; - return ( - - ); + return ( + + ); }; export default Navbar; diff --git a/src/lib/types.d.ts b/src/lib/types.d.ts new file mode 100644 index 0000000..2620ee0 --- /dev/null +++ b/src/lib/types.d.ts @@ -0,0 +1,6 @@ +export type Event = { + id: string; + title: string; + start?: Date; + location?: string; +}; From eec928097786945e4c6421176b284a929622625d Mon Sep 17 00:00:00 2001 From: Atiqur Rahman Date: Wed, 2 Oct 2024 12:07:26 -0500 Subject: [PATCH 2/2] add calendar in events page --- src/app/api/events/route.ts | 3 +- src/app/events/page.tsx | 19 +++++++ src/components/events/Calendar.tsx | 68 ++++++++++++++++++++++++++ src/components/events/CalendarBody.tsx | 60 +++++++++++++++++++++++ src/components/events/EventDays.tsx | 37 ++++++++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 src/app/events/page.tsx create mode 100644 src/components/events/Calendar.tsx create mode 100644 src/components/events/CalendarBody.tsx create mode 100644 src/components/events/EventDays.tsx diff --git a/src/app/api/events/route.ts b/src/app/api/events/route.ts index a566169..ed3b8fc 100644 --- a/src/app/api/events/route.ts +++ b/src/app/api/events/route.ts @@ -8,7 +8,8 @@ const endpoint = `https://www.googleapis.com/calendar/v3/calendars/${calendarId} export async function GET() { // Fetch and parse data const response = await fetch( - `${endpoint}?key=${process.env.CALENDAR_API_KEY}` + `${endpoint}?key=${process.env.CALENDAR_API_KEY}`, + { cache: "no-store" } ); const data = await response.json(); diff --git a/src/app/events/page.tsx b/src/app/events/page.tsx new file mode 100644 index 0000000..afc2f1b --- /dev/null +++ b/src/app/events/page.tsx @@ -0,0 +1,19 @@ +import Calendar from "@/components/events/Calendar"; +type Params = { + month: string | null; + year: string | null; +}; + +export default function Events({ searchParams }: { searchParams: Params }) { + const today = new Date(); + const month = parseInt(searchParams.month || today.getMonth().toString()); + const year = parseInt(searchParams.year || today.getFullYear().toString()); + + return ( +
+
+ +
+
+ ); +} diff --git a/src/components/events/Calendar.tsx b/src/components/events/Calendar.tsx new file mode 100644 index 0000000..8e2ae15 --- /dev/null +++ b/src/components/events/Calendar.tsx @@ -0,0 +1,68 @@ +import Link from "next/link"; +import CalendarBody from "./CalendarBody"; +import { byDate } from "@/app/api/events/byDate/route"; + +type Props = { + month: number; + year: number; +}; + +const monthNames = [ + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +]; + +function changeMonth( + amt: "inc" | "dec", + { month, year }: { month: number; year: number } +) { + if (amt === "inc") { + if (month === 11) return { month: 0, year: year + 1 }; + return { month: month + 1, year }; + } + if (month === 0) return { month: 11, year: year - 1 }; + return { month: month - 1, year }; +} + +async function Calendar({ month, year }: Props) { + const events = await byDate(month, year); + const nextParams = changeMonth("inc", { month, year }); + const prevParams = changeMonth("dec", { month, year }); + + return ( +
+
+ {`<`} + + {monthNames[month]} + + {`>`} +
+ +
+ ); +} + +export default Calendar; diff --git a/src/components/events/CalendarBody.tsx b/src/components/events/CalendarBody.tsx new file mode 100644 index 0000000..8e2c2b2 --- /dev/null +++ b/src/components/events/CalendarBody.tsx @@ -0,0 +1,60 @@ +import EventComponent from "@/components/events/EventDays"; +import type { Event } from "@/lib/types.d.ts"; + +const daysOfWeek = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; + +type Props = { + year: number; + month: number; + events: Event[]; +}; + +export default function CalendarBody({ year, month, events }: Props) { + const firstDayOfMonth = new Date(year, month, 1); + const lastDayOfMonth = new Date(year, month + 1, 0); + + const firstDayIndex = firstDayOfMonth.getDay(); + const lastDayIndex = lastDayOfMonth.getDay(); + const lastDay = lastDayOfMonth.getDate(); + + const daysInMonth = Array.from({ length: lastDay }, (_, i) => i + 1); + + return ( +
+ {daysOfWeek.map((day) => ( +
+ {day} +
+ ))} + {Array.from({ length: firstDayIndex }).map((_, i) => ( +
+ ))} + {daysInMonth.map((day) => ( +
+

+ {day} +

+
+ {events.map((event) => ( + + ))} +
+
+ ))} + {Array.from({ length: 6 - lastDayIndex }).map((_, i) => ( +
+ ))} +
+ ); +} diff --git a/src/components/events/EventDays.tsx b/src/components/events/EventDays.tsx new file mode 100644 index 0000000..3edf912 --- /dev/null +++ b/src/components/events/EventDays.tsx @@ -0,0 +1,37 @@ +"use client"; + +import { useEffect, useState } from "react"; +import type { Event } from "@/lib/types.d.ts"; + +type Props = { + event: Event; + day: number; + month: number; +}; + +const EventComponent = ({ event, day, month }: Props) => { + const [localStartDate, setLocalStartDate] = useState(null); + + useEffect(() => { + if (event.start) { + const localDate = new Date(event.start); + setLocalStartDate(localDate); + } + }, [event]); + + if ( + !localStartDate || + localStartDate.getDate() !== day || + localStartDate.getMonth() !== month + ) { + return null; + } + + return ( +
+ {event.title} +
+ ); +}; + +export default EventComponent;