From 00b5b50508f056d35a2e62c9b07fdadffb5e2d3e Mon Sep 17 00:00:00 2001 From: Krisha Date: Sun, 8 Feb 2026 12:56:48 -0500 Subject: [PATCH 1/8] feat: event layout api integration partial doneness --- frontend/app/events/EventSection.tsx | 113 +++++++++++++++------------ frontend/app/events/page.tsx | 77 +++++++++++++----- 2 files changed, 120 insertions(+), 70 deletions(-) diff --git a/frontend/app/events/EventSection.tsx b/frontend/app/events/EventSection.tsx index 96b2046..70c2466 100644 --- a/frontend/app/events/EventSection.tsx +++ b/frontend/app/events/EventSection.tsx @@ -1,25 +1,30 @@ -import EventCard from "./EventCard"; +import React from "react"; +import { Event } from "./page"; +import { FaRegCalendarAlt } from "react-icons/fa"; -type eventSectionProp = { - isCurrent: Boolean; -} - -//TODO: Add list into prop for events that will be passed down into page +type EventSectionProps = { + isCurrent: boolean; + events: Event[]; + loading?: boolean; +}; -export default function EventSection({ isCurrent }: eventSectionProp) { - const events = [ - { - monthLabel: "SEPTEMBER 2025", - weekday: "MON", - day: "25", - meta: "September 25 Location: IC Atrium @ 3:00 pm - 8:00 pm", - title: - "Antimicrobial Resistance Across Canada: Youth Leading Change in Policy and Practice", - description: - "Join SCWIST, CanAMR Net, and EPIC on November 17 during National AMR Week for a special event hosted. Learn how to think like a recruiter with WiCSM x Bell! Listen to and network with a panel of tech professionals.", - posterSrc: "/globe.svg", - }, - ]; +export default function EventSection({ + isCurrent, + events, + loading, +}: EventSectionProps) { + // Helper to format date and weekday + function getDateParts(dateStr: string) { + const date = new Date(dateStr); + const monthLabel = date + .toLocaleString("default", { month: "long", year: "numeric" }) + .toUpperCase(); + const weekday = date + .toLocaleString("default", { weekday: "short" }) + .toUpperCase(); + const day = date.getDate().toString(); + return { monthLabel, weekday, day }; + } return (
@@ -27,37 +32,43 @@ export default function EventSection({ isCurrent }: eventSectionProp) { {isCurrent ? "Current Events" : "Past Events"}
- {/* UNCOMMENT WHEN INTEGRATING API INTO EVENTS */} - {/* {currentEvents.map((events) => ( - - ))} */} - - - + {loading ? ( +
+ + + Loading events... + +
+ ) : events.length === 0 ? ( +
+ + + No events found. + +
+ ) : ( + events.map((event) => { + //Extract date information + const { monthLabel, weekday, day } = getDateParts(event.date); + return ( + + ); + }) + )}
); diff --git a/frontend/app/events/page.tsx b/frontend/app/events/page.tsx index 7da0e64..03bc413 100644 --- a/frontend/app/events/page.tsx +++ b/frontend/app/events/page.tsx @@ -1,33 +1,72 @@ +"use client"; import EventCard from "./EventCard"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; import EventSection from "./EventSection"; +import { useEffect, useState } from "react"; + +type Event = { + id: string; + title: string; + description: string | null; + date: string; + start_time: string | null; + end_time: string | null; + location: string | null; + image_url: string | null; + signup_url: string | null; +}; export default function EventsPage() { - //Sample data for dev looking - const events = [ - { - monthLabel: "SEPTEMBER 2025", - weekday: "MON", - day: "25", - meta: "September 25 Location: IC Atrium @ 3:00 pm - 8:00 pm", - title: - "Antimicrobial Resistance Across Canada: Youth Leading Change in Policy and Practice", - description: - "Join SCWIST, CanAMR Net, and EPIC on November 17 during National AMR Week for a special event hosted. Learn how to think like a recruiter with WiCSM x Bell! Listen to and network with a panel of tech professionals.", - posterSrc: "/globe.svg", - }, - ]; - - //TODO: Add supabase call for past and current events, create event type and pass down into component - //USE useEffect/suspene + const [upcomingEvents, setUpcomingEvents] = useState([]); + const [pastEvents, setPastEvents] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + async function fetchEvents() { + setLoading(true); + try { + const baseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL || "https://dummy.supabase.co"; + const headers = { Authorization: `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`,}; + + const upcomingRes = await fetch( + `${baseUrl}/functions/v1/events/upcoming`, + { headers } + ); + + const pastRes = await fetch( + `${baseUrl}/functions/v1/events/past`, + { headers } + ); + + const upcomingData = await upcomingRes.json(); + const pastData = await pastRes.json(); + + setUpcomingEvents(upcomingData.events || []); + setPastEvents(pastData.events || []); + + } catch (err) { + // handle error + console.log(err); + setUpcomingEvents([]); + setPastEvents([]); + } finally { + setLoading(false); + } + } + fetchEvents(); + }, []); return (
- - + +
From 5783d5b1ef3caab7adc735ac04de4197b466659f Mon Sep 17 00:00:00 2001 From: Krisha Date: Wed, 18 Feb 2026 15:48:17 -0500 Subject: [PATCH 2/8] feat: events api integration into page --- frontend/app/api/events/route.ts | 60 ++++++++++++ frontend/app/events/EventCard.tsx | 7 +- frontend/app/events/EventSection.tsx | 7 +- frontend/app/events/page.tsx | 38 ++++---- frontend/package-lock.json | 140 ++++++++++++++++++++++++++- frontend/package.json | 4 +- 6 files changed, 226 insertions(+), 30 deletions(-) create mode 100644 frontend/app/api/events/route.ts diff --git a/frontend/app/api/events/route.ts b/frontend/app/api/events/route.ts new file mode 100644 index 0000000..5bcaeda --- /dev/null +++ b/frontend/app/api/events/route.ts @@ -0,0 +1,60 @@ +import { NextResponse } from "next/server"; + +export async function GET() { + try { + const supabaseUrl = process.env.SUPABASE_URL || ""; + const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || ""; + const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || ""; + if (!supabaseUrl || !supabaseServiceKey || !supabaseAnonKey) { + return NextResponse.json({ + upcoming: [], + past: [], + error: "Supabase credentials missing", + }); + } + const headers = { + Authorization: `Bearer ${supabaseAnonKey}`, + "Content-Type": "application/json", + }; + + // Fetch upcoming events + const upcomingRes = await fetch( + `${supabaseUrl}/functions/v1/events/upcoming`, + { + method: "GET", + headers, + }, + ); + const upcomingJson = await upcomingRes.json().catch(() => ({})); + // console.log("upcomingRes status:", upcomingRes.status); + // console.log("upcomingRes body:", upcomingJson); + + // Fetch past events + const pastRes = await fetch(`${supabaseUrl}/functions/v1/events/past`, { + method: "GET", + headers, + }); + const pastJson = await pastRes.json().catch(() => ({})); + // console.log("pastRes status:", pastRes.status); + // console.log("pastRes body:", pastJson); + + return NextResponse.json({ + upcoming: upcomingJson.events || [], + past: pastJson.events || [], + upcomingDebug: { + status: upcomingRes.status, + body: upcomingJson, + }, + pastDebug: { + status: pastRes.status, + body: pastJson, + }, + }); + } catch (err) { + return NextResponse.json({ + upcoming: [], + past: [], + error: err?.message || "Unknown error", + }); + } +} diff --git a/frontend/app/events/EventCard.tsx b/frontend/app/events/EventCard.tsx index 31f99d4..9493b5e 100644 --- a/frontend/app/events/EventCard.tsx +++ b/frontend/app/events/EventCard.tsx @@ -10,10 +10,11 @@ type EventCardProps = { description: string; posterSrc?: string; posterAlt?: string; - titleLink?: string; // ← OPTIONAL link + titleLink?: string; }; -const EventCard: React.FC = ({ + +function EventCard({ monthLabel, weekday, day, @@ -23,7 +24,7 @@ const EventCard: React.FC = ({ posterSrc, posterAlt, titleLink, -}) => { +}: EventSectionProps) { return (
{/* Top month header */} diff --git a/frontend/app/events/EventSection.tsx b/frontend/app/events/EventSection.tsx index 70c2466..fecc3d2 100644 --- a/frontend/app/events/EventSection.tsx +++ b/frontend/app/events/EventSection.tsx @@ -1,5 +1,6 @@ import React from "react"; import { Event } from "./page"; +import EventCard from "./EventCard"; import { FaRegCalendarAlt } from "react-icons/fa"; type EventSectionProps = { @@ -15,7 +16,9 @@ export default function EventSection({ }: EventSectionProps) { // Helper to format date and weekday function getDateParts(dateStr: string) { - const date = new Date(dateStr); + // Parse as local date [avoid off-by-one error] + const [year, month, dayNum] = dateStr.split("-").map(Number); + const date = new Date(year, month - 1, dayNum); const monthLabel = date .toLocaleString("default", { month: "long", year: "numeric" }) .toUpperCase(); @@ -48,7 +51,7 @@ export default function EventSection({
) : ( events.map((event) => { - //Extract date information + //Extract date information const { monthLabel, weekday, day } = getDateParts(event.date); return ( =20.0.0" + } + }, + "node_modules/@supabase/functions-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.97.0.tgz", + "integrity": "sha512-fSaA0ZeBUS9hMgpGZt5shIZvfs3Mvx2ZdajQT4kv/whubqDBAp3GU5W8iIXy21MRvKmO2NpAj8/Q6y+ZkZyF/w==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/postgrest-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.97.0.tgz", + "integrity": "sha512-g4Ps0eaxZZurvfv/KGoo2XPZNpyNtjth9aW8eho9LZWM0bUuBtxPZw3ZQ6ERSpEGogshR+XNgwlSPIwcuHCNww==", + "license": "MIT", + "dependencies": { + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/realtime-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.97.0.tgz", + "integrity": "sha512-37Jw0NLaFP0CZd7qCan97D1zWutPrTSpgWxAw6Yok59JZoxp4IIKMrPeftJ3LZHmf+ILQOPy3i0pRDHM9FY36Q==", + "license": "MIT", + "dependencies": { + "@types/phoenix": "^1.6.6", + "@types/ws": "^8.18.1", + "tslib": "2.8.1", + "ws": "^8.18.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/storage-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.97.0.tgz", + "integrity": "sha512-9f6NniSBfuMxOWKwEFb+RjJzkfMdJUwv9oHuFJKfe/5VJR8cd90qw68m6Hn0ImGtwG37TUO+QHtoOechxRJ1Yg==", + "license": "MIT", + "dependencies": { + "iceberg-js": "^0.8.1", + "tslib": "2.8.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@supabase/supabase-js": { + "version": "2.97.0", + "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.97.0.tgz", + "integrity": "sha512-kTD91rZNO4LvRUHv4x3/4hNmsEd2ofkYhuba2VMUPRVef1RCmnHtm7rIws38Fg0yQnOSZOplQzafn0GSiy6GVg==", + "license": "MIT", + "dependencies": { + "@supabase/auth-js": "2.97.0", + "@supabase/functions-js": "2.97.0", + "@supabase/postgrest-js": "2.97.0", + "@supabase/realtime-js": "2.97.0", + "@supabase/storage-js": "2.97.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -1612,12 +1694,17 @@ "version": "20.19.25", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.25.tgz", "integrity": "sha512-ZsJzA5thDQMSQO788d7IocwwQbI8B5OPzmqNvpf3NY/+MHDAS759Wo0gd2WQeXYt5AAAQjzcrTVC6SKCuYgoCQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" } }, + "node_modules/@types/phoenix": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz", + "integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==", + "license": "MIT" + }, "node_modules/@types/react": { "version": "19.2.5", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", @@ -1639,6 +1726,15 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/ws": { + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.46.4", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", @@ -4033,6 +4129,15 @@ "hermes-estree": "0.25.1" } }, + "node_modules/iceberg-js": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", + "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -5552,6 +5657,15 @@ "react": "^19.2.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -6458,7 +6572,6 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - "dev": true, "license": "MIT" }, "node_modules/unrs-resolver": { @@ -6652,6 +6765,27 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index e908c6b..e3b1858 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,11 +9,13 @@ "lint": "eslint" }, "dependencies": { + "@supabase/supabase-js": "^2.97.0", "@upstash/ratelimit": "^2.0.7", "lucide-react": "^0.554.0", "next": "16.0.10", "react": "19.2.0", - "react-dom": "19.2.0" + "react-dom": "19.2.0", + "react-icons": "^5.5.0" }, "devDependencies": { "@tailwindcss/postcss": "^4", From 9c0f3f8be792107a7c6290a2852b25a573d51601 Mon Sep 17 00:00:00 2001 From: Krisha Date: Wed, 18 Feb 2026 15:56:30 -0500 Subject: [PATCH 3/8] feat: added error state component --- frontend/app/api/events/route.ts | 4 +- frontend/app/events/page.tsx | 60 +++++++++++++++++++++++------- frontend/components/ErrorState.tsx | 44 ++++++++++++++++++++++ 3 files changed, 92 insertions(+), 16 deletions(-) create mode 100644 frontend/components/ErrorState.tsx diff --git a/frontend/app/api/events/route.ts b/frontend/app/api/events/route.ts index 5bcaeda..106673a 100644 --- a/frontend/app/api/events/route.ts +++ b/frontend/app/api/events/route.ts @@ -35,8 +35,8 @@ export async function GET() { headers, }); const pastJson = await pastRes.json().catch(() => ({})); - // console.log("pastRes status:", pastRes.status); - // console.log("pastRes body:", pastJson); + console.log("pastRes status:", pastRes.status); + console.log("pastRes body:", pastJson); return NextResponse.json({ upcoming: upcomingJson.events || [], diff --git a/frontend/app/events/page.tsx b/frontend/app/events/page.tsx index 20d5b39..69a6251 100644 --- a/frontend/app/events/page.tsx +++ b/frontend/app/events/page.tsx @@ -2,6 +2,7 @@ import EventCard from "./EventCard"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; +import ErrorState from "@/components/ErrorState"; import EventSection from "./EventSection"; import { useEffect, useState } from "react"; @@ -21,29 +22,39 @@ export default function EventsPage() { const [upcomingEvents, setUpcomingEvents] = useState([]); const [pastEvents, setPastEvents] = useState([]); const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); useEffect(() => { async function fetchEvents() { setLoading(true); + setError(null); try { const res = await fetch("/api/events"); - // console.log("/api/events response status:", res.status); const text = await res.text(); - // console.log("/api/events raw response:", text); let data; try { data = JSON.parse(text); } catch (parseErr) { - console.log("Failed to parse JSON:", parseErr); + setError( + "Unable to load events at this time. Please try again later.", + ); setUpcomingEvents([]); setPastEvents([]); + setLoading(false); return; } - // console.log("Parsed events data:", data); - setUpcomingEvents(data.upcoming || []); - setPastEvents(data.past || []); + if (!res.ok || data.error) { + setError( + "Unable to load events at this time. Please try again later.", + ); + setUpcomingEvents([]); + setPastEvents([]); + } else { + setUpcomingEvents(data.upcoming || []); + setPastEvents(data.past || []); + } } catch (err) { - // console.log("fetchEvents error:", err); + setError("Unable to load events at this time. Please try again later."); setUpcomingEvents([]); setPastEvents([]); } finally { @@ -56,13 +67,34 @@ export default function EventsPage() { return (
-
- - +
+
+

+ Events +

+

+ Stay up to date with our latest events. +

+
+ {error ? ( + window.location.reload()} + /> + ) : ( + <> + + + + )}
diff --git a/frontend/components/ErrorState.tsx b/frontend/components/ErrorState.tsx new file mode 100644 index 0000000..f043a42 --- /dev/null +++ b/frontend/components/ErrorState.tsx @@ -0,0 +1,44 @@ + +type ErrorStateProps = { + message?: string; + subtitle?: string; + onRetry?: () => void; + retryLabel?: string; +}; + +function ErrorState({ + message = "Unable to load content at this time. Please try again later!", + subtitle = "Something went wrong on our end :(", + onRetry, + retryLabel = "Try Again", +}: ErrorStateProps) { + return ( +
+ + + + {message} + {subtitle} + {onRetry && ( + + )} +
+ ); +} + +export default ErrorState; From 8df75203235e3304fda28d6a5c36df052349af2b Mon Sep 17 00:00:00 2001 From: Krisha Date: Fri, 20 Feb 2026 23:29:45 -0500 Subject: [PATCH 4/8] feat: enhanced events page and api routing --- frontend/app/api/events/[id]/route.ts | 46 ++++++ frontend/app/api/events/past/route.ts | 40 ++++++ frontend/app/api/events/route.ts | 90 ++++++------ frontend/app/api/events/service.ts | 120 ++++++++++++++++ frontend/app/api/events/upcoming/route.ts | 40 ++++++ frontend/app/api/utils/supabase.ts | 90 ++++++++++++ frontend/app/events/EventCard.tsx | 2 + frontend/app/events/EventSection.tsx | 167 +++++++++++++++++----- frontend/app/events/page.tsx | 50 +++---- frontend/app/events/types.ts | 16 +++ frontend/app/events/utils.ts | 31 ++++ frontend/components/ErrorState.tsx | 9 +- frontend/package-lock.json | 43 ++++++ frontend/package.json | 1 + 14 files changed, 624 insertions(+), 121 deletions(-) create mode 100644 frontend/app/api/events/[id]/route.ts create mode 100644 frontend/app/api/events/past/route.ts create mode 100644 frontend/app/api/events/service.ts create mode 100644 frontend/app/api/events/upcoming/route.ts create mode 100644 frontend/app/api/utils/supabase.ts create mode 100644 frontend/app/events/types.ts create mode 100644 frontend/app/events/utils.ts diff --git a/frontend/app/api/events/[id]/route.ts b/frontend/app/api/events/[id]/route.ts new file mode 100644 index 0000000..a7e537f --- /dev/null +++ b/frontend/app/api/events/[id]/route.ts @@ -0,0 +1,46 @@ +/** + * Route: GET /api/events/:id + * Purpose: Return one event by its identifier. + * Behavior: + * - Validates Supabase service-role credentials. + * - Reads the id from route params and fetches a single record. + * - Returns 404 when the event is not found. + */ +import { NextResponse } from "next/server"; +import { fetchEventById } from "../service"; +import { + getSupabaseCredentials, + hasMissingServiceCredentials, +} from "../../utils/supabase"; + +type RouteContext = { + params: { + id: string; + }; +}; + +export async function GET(_request: Request, { params }: RouteContext) { + try { + const credentials = getSupabaseCredentials(); + + if (hasMissingServiceCredentials(credentials)) { + return NextResponse.json( + { error: "Supabase credentials missing" }, + { status: 500 }, + ); + } + + const result = await fetchEventById(credentials, params.id); + if (!result.ok) { + return NextResponse.json( + { error: result.error }, + { status: result.status }, + ); + } + + return NextResponse.json({ event: result.event }); + } catch (err) { + const message = err instanceof Error ? err.message : "Unknown error"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/frontend/app/api/events/past/route.ts b/frontend/app/api/events/past/route.ts new file mode 100644 index 0000000..985402b --- /dev/null +++ b/frontend/app/api/events/past/route.ts @@ -0,0 +1,40 @@ +/** + * Route: GET /api/events/past + * Purpose: Return past events only. + * Behavior: + * - Validates Supabase anon credentials. + * - Proxies to Supabase Edge Function events/past. + * - Returns normalized events array with optional debug/error metadata. + */ +import { NextResponse } from "next/server"; +import { fetchPastEvents } from "../service"; +import { + getSupabaseCredentials, + hasMissingAnonCredentials, +} from "../../utils/supabase"; + +export async function GET() { + try { + const credentials = getSupabaseCredentials(); + + if (hasMissingAnonCredentials(credentials)) { + return NextResponse.json( + { events: [], error: "Supabase credentials missing" }, + { status: 500 }, + ); + } + + const result = await fetchPastEvents(credentials); + return NextResponse.json( + { + events: result.events || [], + debug: result.debug, + error: result.error, + }, + { status: result.ok ? 200 : result.status }, + ); + } catch (err) { + const message = err instanceof Error ? err.message : "Unknown error"; + return NextResponse.json({ events: [], error: message }, { status: 500 }); + } +} diff --git a/frontend/app/api/events/route.ts b/frontend/app/api/events/route.ts index 106673a..0730834 100644 --- a/frontend/app/api/events/route.ts +++ b/frontend/app/api/events/route.ts @@ -1,60 +1,54 @@ +/** + * Route: GET /api/events + * Purpose: Return both upcoming and past events in a single response. + * Behavior: + * - Validates required Supabase anon credentials. + * - Fetches upcoming and past datasets in parallel. + * - Returns combined payload with optional debug/error metadata. + */ import { NextResponse } from "next/server"; +import { fetchPastEvents, fetchUpcomingEvents } from "./service"; +import { + getSupabaseCredentials, + hasMissingAnonCredentials, +} from "../utils/supabase"; export async function GET() { try { - const supabaseUrl = process.env.SUPABASE_URL || ""; - const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY || ""; - const supabaseAnonKey = process.env.SUPABASE_ANON_KEY || ""; - if (!supabaseUrl || !supabaseServiceKey || !supabaseAnonKey) { - return NextResponse.json({ - upcoming: [], - past: [], - error: "Supabase credentials missing", - }); + const credentials = getSupabaseCredentials(); + + if (hasMissingAnonCredentials(credentials)) { + return NextResponse.json( + { upcoming: [], past: [], error: "Supabase credentials missing" }, + { status: 500 }, + ); } - const headers = { - Authorization: `Bearer ${supabaseAnonKey}`, - "Content-Type": "application/json", - }; - // Fetch upcoming events - const upcomingRes = await fetch( - `${supabaseUrl}/functions/v1/events/upcoming`, + const [upcomingResult, pastResult] = await Promise.all([ + fetchUpcomingEvents(credentials), + fetchPastEvents(credentials), + ]); + + const hasError = !upcomingResult.ok || !pastResult.ok; + return NextResponse.json( { - method: "GET", - headers, + upcoming: upcomingResult.events || [], + past: pastResult.events || [], + upcomingDebug: upcomingResult.debug, + pastDebug: pastResult.debug, + error: upcomingResult.error || pastResult.error, }, + { status: hasError ? 500 : 200 }, ); - const upcomingJson = await upcomingRes.json().catch(() => ({})); - // console.log("upcomingRes status:", upcomingRes.status); - // console.log("upcomingRes body:", upcomingJson); - - // Fetch past events - const pastRes = await fetch(`${supabaseUrl}/functions/v1/events/past`, { - method: "GET", - headers, - }); - const pastJson = await pastRes.json().catch(() => ({})); - console.log("pastRes status:", pastRes.status); - console.log("pastRes body:", pastJson); - - return NextResponse.json({ - upcoming: upcomingJson.events || [], - past: pastJson.events || [], - upcomingDebug: { - status: upcomingRes.status, - body: upcomingJson, - }, - pastDebug: { - status: pastRes.status, - body: pastJson, - }, - }); } catch (err) { - return NextResponse.json({ - upcoming: [], - past: [], - error: err?.message || "Unknown error", - }); + const message = err instanceof Error ? err.message : "Unknown error"; + return NextResponse.json( + { + upcoming: [], + past: [], + error: message, + }, + { status: 500 }, + ); } } diff --git a/frontend/app/api/events/service.ts b/frontend/app/api/events/service.ts new file mode 100644 index 0000000..f6396eb --- /dev/null +++ b/frontend/app/api/events/service.ts @@ -0,0 +1,120 @@ +/** + * Events service layer. + * + * Purpose: + * - Centralize all event data-access calls used by API routes. + * - Keep route files focused on HTTP orchestration and status mapping. + * + * Notes: + * - This service intentionally uses Supabase Edge Functions for event reads + * (`events/upcoming`, `events/past`, `events/:id`) to keep behavior consistent + * with backend function logic. + * - Response parsing is defensive to support minor payload-shape differences + * between edge function handlers. + */ +import { + requestSupabaseEdgeFunction, + type SupabaseCredentials, +} from "../utils/supabase"; + +const EVENT_SELECT_FIELDS = + "id,title,description,date,start_time,end_time,location,image_url,signup_url,created_at,updated_at"; + +type EventsTimeline = "upcoming" | "past"; + +/** + * Fetches a single event by id through the Edge Function route. + * + * Expected successful payloads supported: + * - `{ event: {...} }` + * - `{ events: [{...}] }` + * - direct object payload + * + * Returns a normalized result with `ok`, `status`, and either `event` or `error`. + */ +export async function fetchEventById( + credentials: SupabaseCredentials, + eventId: string, +) { + const encodedId = encodeURIComponent(eventId); + const { response, body } = await requestSupabaseEdgeFunction( + credentials.url, + `events/${encodedId}`, + credentials.anonKey, + { method: "GET" }, + ); + + if (!response.ok) { + return { + ok: false as const, + status: response.status, + error: + (body as { error?: string; message?: string })?.error || + (body as { error?: string; message?: string })?.message || + "Failed to fetch event", + }; + } + + const maybeObject = body as { + event?: unknown; + events?: unknown[]; + }; + + const event = + maybeObject?.event || + (Array.isArray(maybeObject?.events) ? maybeObject.events[0] : null) || + (Array.isArray(body) ? body[0] : body); + + if (!event) { + return { + ok: false as const, + status: 404, + error: "Event not found", + }; + } + + return { + ok: true as const, + event, + }; +} + +/** + * Fetches events from one timeline bucket (`upcoming` or `past`) through + * the Edge Function route and returns a normalized response contract. + */ +export async function fetchEventsByTimeline( + credentials: SupabaseCredentials, + timeline: EventsTimeline, +) { + const { response, body } = await requestSupabaseEdgeFunction( + credentials.url, + `events/${timeline}`, + credentials.anonKey, + ); + + return { + ok: response.ok, + status: response.status, + events: body.events || [], + debug: { + status: response.status, + body, + }, + error: (body as { error?: string })?.error, + }; +} + +/** + * Convenience wrapper for fetching upcoming events. + */ +export async function fetchUpcomingEvents(credentials: SupabaseCredentials) { + return fetchEventsByTimeline(credentials, "upcoming"); +} + +/** + * Convenience wrapper for fetching past events. + */ +export async function fetchPastEvents(credentials: SupabaseCredentials) { + return fetchEventsByTimeline(credentials, "past"); +} diff --git a/frontend/app/api/events/upcoming/route.ts b/frontend/app/api/events/upcoming/route.ts new file mode 100644 index 0000000..da153f0 --- /dev/null +++ b/frontend/app/api/events/upcoming/route.ts @@ -0,0 +1,40 @@ +/** + * Route: GET /api/events/upcoming + * Purpose: Return upcoming events only. + * Behavior: + * - Validates Supabase anon credentials. + * - Proxies to Supabase Edge Function events/upcoming. + * - Returns normalized events array with optional debug/error metadata. + */ +import { NextResponse } from "next/server"; +import { fetchUpcomingEvents } from "../service"; +import { + getSupabaseCredentials, + hasMissingAnonCredentials, +} from "../../utils/supabase"; + +export async function GET() { + try { + const credentials = getSupabaseCredentials(); + + if (hasMissingAnonCredentials(credentials)) { + return NextResponse.json( + { events: [], error: "Supabase credentials missing" }, + { status: 500 }, + ); + } + + const result = await fetchUpcomingEvents(credentials); + return NextResponse.json( + { + events: result.events || [], + debug: result.debug, + error: result.error, + }, + { status: result.ok ? 200 : result.status }, + ); + } catch (err) { + const message = err instanceof Error ? err.message : "Unknown error"; + return NextResponse.json({ events: [], error: message }, { status: 500 }); + } +} diff --git a/frontend/app/api/utils/supabase.ts b/frontend/app/api/utils/supabase.ts new file mode 100644 index 0000000..102c278 --- /dev/null +++ b/frontend/app/api/utils/supabase.ts @@ -0,0 +1,90 @@ +export type SupabaseCredentials = { + url: string; + serviceKey: string; + anonKey: string; +}; + +export type EdgeRequestOptions = { + method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; + body?: unknown; +}; + +export function getSupabaseCredentials(): SupabaseCredentials { + return { + url: process.env.SUPABASE_URL || "", + serviceKey: process.env.SUPABASE_SERVICE_ROLE_KEY || "", + anonKey: process.env.SUPABASE_ANON_KEY || "", + }; +} + +export function hasMissingAnonCredentials( + credentials: SupabaseCredentials, +): boolean { + return !credentials.url || !credentials.anonKey; +} + +export function hasMissingServiceCredentials( + credentials: SupabaseCredentials, +): boolean { + return !credentials.url || !credentials.serviceKey; +} + +function getSupabaseAnonHeaders(anonKey: string) { + return { + Authorization: `Bearer ${anonKey}`, + "Content-Type": "application/json", + }; +} + +function getSupabaseServiceHeaders(serviceKey: string) { + return { + Authorization: `Bearer ${serviceKey}`, + apikey: serviceKey, + "Content-Type": "application/json", + }; +} + +/** + * Core network helper for Supabase Edge Functions. + * Use this for calls to /functions/v1/* endpoints with anon-key authorization. + * Returns the raw response plus a safely parsed JSON payload (falls back to {}). + */ +export async function requestSupabaseEdgeFunction( + baseUrl: string, + path: string, + anonKey: string, + options: EdgeRequestOptions = {}, +) { + const { method = "GET", body } = options; + + const response = await fetch(`${baseUrl}/functions/v1/${path}`, { + method, + headers: getSupabaseAnonHeaders(anonKey), + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + + const payload = await response.json().catch(() => ({})); + return { response, body: payload }; +} + +/** + * Core network helper for Supabase REST API. + * Use this for calls to /rest/v1/* endpoints with service-role authorization. + * Returns the raw response plus a safely parsed JSON payload (falls back to []). + */ +export async function requestSupabaseRest( + credentials: SupabaseCredentials, + path: string, + options: EdgeRequestOptions = {}, +) { + const { method = "GET", body } = options; + + const response = await fetch(`${credentials.url}/rest/v1/${path}`, { + method, + headers: getSupabaseServiceHeaders(credentials.serviceKey), + body: body !== undefined ? JSON.stringify(body) : undefined, + }); + + const payload = await response.json().catch(() => []); + return { response, body: payload }; +} diff --git a/frontend/app/events/EventCard.tsx b/frontend/app/events/EventCard.tsx index 9493b5e..7e64fc6 100644 --- a/frontend/app/events/EventCard.tsx +++ b/frontend/app/events/EventCard.tsx @@ -2,6 +2,7 @@ import React from "react"; import "./EventCard.css"; type EventCardProps = { + id: string; // id is needed for redirect to event-specfic page monthLabel: string; weekday: string; day: string; @@ -15,6 +16,7 @@ type EventCardProps = { function EventCard({ + id, monthLabel, weekday, day, diff --git a/frontend/app/events/EventSection.tsx b/frontend/app/events/EventSection.tsx index fecc3d2..1f7f563 100644 --- a/frontend/app/events/EventSection.tsx +++ b/frontend/app/events/EventSection.tsx @@ -1,7 +1,20 @@ -import React from "react"; -import { Event } from "./page"; +import React, { useEffect, useState } from "react"; +import type { Event } from "./types"; import EventCard from "./EventCard"; import { FaRegCalendarAlt } from "react-icons/fa"; +import { + AnimatePresence, + LazyMotion, + domAnimation, + m, + useReducedMotion, +} from "framer-motion"; +import { + EVENTS_PER_PAGE, + getDateParts, + getPaginatedEvents, + getTotalPages, +} from "./utils"; type EventSectionProps = { isCurrent: boolean; @@ -14,19 +27,38 @@ export default function EventSection({ events, loading, }: EventSectionProps) { - // Helper to format date and weekday - function getDateParts(dateStr: string) { - // Parse as local date [avoid off-by-one error] - const [year, month, dayNum] = dateStr.split("-").map(Number); - const date = new Date(year, month - 1, dayNum); - const monthLabel = date - .toLocaleString("default", { month: "long", year: "numeric" }) - .toUpperCase(); - const weekday = date - .toLocaleString("default", { weekday: "short" }) - .toUpperCase(); - const day = date.getDate().toString(); - return { monthLabel, weekday, day }; + const [page, setPage] = useState(1); + const [pageDirection, setPageDirection] = useState(1); + const shouldReduceMotion = useReducedMotion(); + const totalPages = getTotalPages(events, EVENTS_PER_PAGE); + const paginatedEvents = getPaginatedEvents(events, page, EVENTS_PER_PAGE); + + useEffect(() => { + setPage((prevPage) => Math.min(prevPage, totalPages)); + }, [totalPages]); + + const pageAnimationVariants = { + enter: (direction: number) => ({ + opacity: 0, + x: shouldReduceMotion ? 0 : direction * 20, + }), + center: { + opacity: 1, + x: 0, + }, + exit: (direction: number) => ({ + opacity: 0, + x: shouldReduceMotion ? 0 : direction * -20, + }), + }; + + function updatePage(nextPage: number) { + if (nextPage < 1 || nextPage > totalPages || nextPage === page) { + return; + } + + setPageDirection(nextPage > page ? 1 : -1); + setPage(nextPage); } return ( @@ -50,27 +82,90 @@ export default function EventSection({ ) : ( - events.map((event) => { - //Extract date information - const { monthLabel, weekday, day } = getDateParts(event.date); - return ( - - ); - }) + <> + + + + {paginatedEvents.map((event) => { + const { monthLabel, weekday, day } = getDateParts( + event.date, + ); + + return ( + + ); + })} + + + + + {/* Pagination controls — only shown when more than one page */} + {totalPages > 1 && ( +
+ + + {Array.from({ length: totalPages }, (_, i) => i + 1).map( + (num) => ( + + ), + )} + + +
+ )} + )} diff --git a/frontend/app/events/page.tsx b/frontend/app/events/page.tsx index 69a6251..e0285b6 100644 --- a/frontend/app/events/page.tsx +++ b/frontend/app/events/page.tsx @@ -1,22 +1,23 @@ "use client"; -import EventCard from "./EventCard"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; import ErrorState from "@/components/ErrorState"; import EventSection from "./EventSection"; import { useEffect, useState } from "react"; +import type { Event, EventsApiResponse } from "./types"; -type Event = { - id: string; - title: string; - description: string | null; - date: string; - start_time: string | null; - end_time: string | null; - location: string | null; - image_url: string | null; - signup_url: string | null; -}; +async function fetchEventBucket(endpoint: string): Promise { + const response = await fetch(endpoint); + const data = (await response + .json() + .catch(() => null)) as EventsApiResponse | null; + + if (!response.ok || !data) { + throw new Error("Failed to fetch events"); + } + + return data; +} export default function EventsPage() { const [upcomingEvents, setUpcomingEvents] = useState([]); @@ -29,29 +30,20 @@ export default function EventsPage() { setLoading(true); setError(null); try { - const res = await fetch("/api/events"); - const text = await res.text(); - let data; - try { - data = JSON.parse(text); - } catch (parseErr) { - setError( - "Unable to load events at this time. Please try again later.", - ); - setUpcomingEvents([]); - setPastEvents([]); - setLoading(false); - return; - } - if (!res.ok || data.error) { + const [upcomingData, pastData] = await Promise.all([ + fetchEventBucket("/api/events/upcoming"), + fetchEventBucket("/api/events/past"), + ]); + + if (upcomingData.error || pastData.error) { setError( "Unable to load events at this time. Please try again later.", ); setUpcomingEvents([]); setPastEvents([]); } else { - setUpcomingEvents(data.upcoming || []); - setPastEvents(data.past || []); + setUpcomingEvents(upcomingData.events || []); + setPastEvents(pastData.events || []); } } catch (err) { setError("Unable to load events at this time. Please try again later."); diff --git a/frontend/app/events/types.ts b/frontend/app/events/types.ts new file mode 100644 index 0000000..5d0537d --- /dev/null +++ b/frontend/app/events/types.ts @@ -0,0 +1,16 @@ +export type Event = { + id: string; + title: string; + description: string | null; + date: string; + start_time: string | null; + end_time: string | null; + location: string | null; + image_url: string | null; + signup_url: string | null; +}; + +export type EventsApiResponse = { + events?: Event[]; + error?: string; +}; diff --git a/frontend/app/events/utils.ts b/frontend/app/events/utils.ts new file mode 100644 index 0000000..2eb40b4 --- /dev/null +++ b/frontend/app/events/utils.ts @@ -0,0 +1,31 @@ +import type { Event } from "./types"; + +export const EVENTS_PER_PAGE = 5; + +export function getDateParts(dateStr: string) { + const [year, month, dayNum] = dateStr.split("-").map(Number); + const date = new Date(year, month - 1, dayNum); + + return { + monthLabel: date + .toLocaleString("default", { month: "long", year: "numeric" }) + .toUpperCase(), + weekday: date.toLocaleString("default", { weekday: "short" }).toUpperCase(), + day: date.getDate().toString(), + }; +} + +export function getTotalPages( + events: Event[], + pageSize: number = EVENTS_PER_PAGE, +) { + return Math.max(1, Math.ceil(events.length / pageSize)); +} + +export function getPaginatedEvents( + events: Event[], + page: number, + pageSize: number = EVENTS_PER_PAGE, +) { + return events.slice((page - 1) * pageSize, page * pageSize); +} diff --git a/frontend/components/ErrorState.tsx b/frontend/components/ErrorState.tsx index f043a42..6638bc6 100644 --- a/frontend/components/ErrorState.tsx +++ b/frontend/components/ErrorState.tsx @@ -29,14 +29,7 @@ function ErrorState({ {message} {subtitle} - {onRetry && ( - - )} + ); } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index b7cab4a..aae4d22 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@supabase/supabase-js": "^2.97.0", "@upstash/ratelimit": "^2.0.7", + "framer-motion": "^12.34.3", "lucide-react": "^0.554.0", "next": "16.0.10", "react": "19.2.0", @@ -3817,6 +3818,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.34.3", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.34.3.tgz", + "integrity": "sha512-v81ecyZKYO/DfpTwHivqkxSUBzvceOpoI+wLfgCgoUIKxlFKEXdg0oR9imxwXumT4SFy8vRk9xzJ5l3/Du/55Q==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.34.3", + "motion-utils": "^12.29.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -5157,6 +5185,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/motion-dom": { + "version": "12.34.3", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.34.3.tgz", + "integrity": "sha512-sYgFe+pR9aIM7o4fhs2aXtOI+oqlUd33N9Yoxcgo1Fv7M20sRkHtCmzE/VRNIcq7uNJ+qio+Xubt1FXH3pQ+eQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.29.2" + } + }, + "node_modules/motion-utils": { + "version": "12.29.2", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.29.2.tgz", + "integrity": "sha512-G3kc34H2cX2gI63RqU+cZq+zWRRPSsNIOjpdl9TN4AQwC4sgwYPl/Q/Obf/d53nOm569T0fYK+tcoSV50BWx8A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/frontend/package.json b/frontend/package.json index e3b1858..3c96c9d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "dependencies": { "@supabase/supabase-js": "^2.97.0", "@upstash/ratelimit": "^2.0.7", + "framer-motion": "^12.34.3", "lucide-react": "^0.554.0", "next": "16.0.10", "react": "19.2.0", From 6d58fb3515b76c331c1f379a7320c34e1b283eba Mon Sep 17 00:00:00 2001 From: Krisha Date: Sat, 21 Feb 2026 00:26:03 -0500 Subject: [PATCH 5/8] fix: fixed based on copilot suggestions on pr --- frontend/app/api/events/past/route.ts | 2 +- frontend/app/api/events/route.ts | 4 ++-- frontend/app/api/events/upcoming/route.ts | 2 +- frontend/app/events/EventCard.tsx | 4 ++-- frontend/app/events/EventSection.tsx | 3 ++- frontend/app/events/page.tsx | 2 +- 6 files changed, 9 insertions(+), 8 deletions(-) diff --git a/frontend/app/api/events/past/route.ts b/frontend/app/api/events/past/route.ts index 985402b..3ee0bac 100644 --- a/frontend/app/api/events/past/route.ts +++ b/frontend/app/api/events/past/route.ts @@ -28,7 +28,7 @@ export async function GET() { return NextResponse.json( { events: result.events || [], - debug: result.debug, + // debug: result.debug, error: result.error, }, { status: result.ok ? 200 : result.status }, diff --git a/frontend/app/api/events/route.ts b/frontend/app/api/events/route.ts index 0730834..2dbe964 100644 --- a/frontend/app/api/events/route.ts +++ b/frontend/app/api/events/route.ts @@ -34,8 +34,8 @@ export async function GET() { { upcoming: upcomingResult.events || [], past: pastResult.events || [], - upcomingDebug: upcomingResult.debug, - pastDebug: pastResult.debug, + // upcomingDebug: upcomingResult.debug, + // pastDebug: pastResult.debug, error: upcomingResult.error || pastResult.error, }, { status: hasError ? 500 : 200 }, diff --git a/frontend/app/api/events/upcoming/route.ts b/frontend/app/api/events/upcoming/route.ts index da153f0..b32b953 100644 --- a/frontend/app/api/events/upcoming/route.ts +++ b/frontend/app/api/events/upcoming/route.ts @@ -28,7 +28,7 @@ export async function GET() { return NextResponse.json( { events: result.events || [], - debug: result.debug, + // debug: result.debug, error: result.error, }, { status: result.ok ? 200 : result.status }, diff --git a/frontend/app/events/EventCard.tsx b/frontend/app/events/EventCard.tsx index 7e64fc6..42e472f 100644 --- a/frontend/app/events/EventCard.tsx +++ b/frontend/app/events/EventCard.tsx @@ -16,7 +16,7 @@ type EventCardProps = { function EventCard({ - id, + _id, //destructure for now (prevent warnings) monthLabel, weekday, day, @@ -26,7 +26,7 @@ function EventCard({ posterSrc, posterAlt, titleLink, -}: EventSectionProps) { +}: EventCardProps) { return (
{/* Top month header */} diff --git a/frontend/app/events/EventSection.tsx b/frontend/app/events/EventSection.tsx index 1f7f563..decdbcc 100644 --- a/frontend/app/events/EventSection.tsx +++ b/frontend/app/events/EventSection.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import type { Event } from "./types"; import EventCard from "./EventCard"; import { FaRegCalendarAlt } from "react-icons/fa"; @@ -110,6 +110,7 @@ export default function EventSection({ return ( Date: Sat, 21 Feb 2026 00:27:52 -0500 Subject: [PATCH 6/8] fix: removed unused vars in error prop --- frontend/components/ErrorState.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/frontend/components/ErrorState.tsx b/frontend/components/ErrorState.tsx index 6638bc6..43c701c 100644 --- a/frontend/components/ErrorState.tsx +++ b/frontend/components/ErrorState.tsx @@ -9,8 +9,6 @@ type ErrorStateProps = { function ErrorState({ message = "Unable to load content at this time. Please try again later!", subtitle = "Something went wrong on our end :(", - onRetry, - retryLabel = "Try Again", }: ErrorStateProps) { return (
From 69fcc0acc46b0c5559820455f1fae02983113d7f Mon Sep 17 00:00:00 2001 From: Krisha Date: Mon, 2 Mar 2026 19:43:17 -0500 Subject: [PATCH 7/8] fix: GET /events/id router issue and package build issues --- frontend/app/api/events/[id]/route.ts | 9 +++++---- frontend/app/api/events/service.ts | 3 --- frontend/app/events/EventCard.css | 6 +++--- frontend/app/events/EventCard.tsx | 20 ++++++++++++------ frontend/app/events/EventSection.tsx | 29 ++++++++++++++------------- frontend/app/events/page.tsx | 7 ++----- frontend/components/ErrorState.tsx | 4 ---- 7 files changed, 39 insertions(+), 39 deletions(-) diff --git a/frontend/app/api/events/[id]/route.ts b/frontend/app/api/events/[id]/route.ts index a7e537f..f4df7b7 100644 --- a/frontend/app/api/events/[id]/route.ts +++ b/frontend/app/api/events/[id]/route.ts @@ -6,7 +6,7 @@ * - Reads the id from route params and fetches a single record. * - Returns 404 when the event is not found. */ -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { fetchEventById } from "../service"; import { getSupabaseCredentials, @@ -14,13 +14,14 @@ import { } from "../../utils/supabase"; type RouteContext = { - params: { + params: Promise<{ id: string; - }; + }>; }; -export async function GET(_request: Request, { params }: RouteContext) { +export async function GET(_request: NextRequest, context: RouteContext) { try { + const params = await context.params; const credentials = getSupabaseCredentials(); if (hasMissingServiceCredentials(credentials)) { diff --git a/frontend/app/api/events/service.ts b/frontend/app/api/events/service.ts index f6396eb..6bfdd97 100644 --- a/frontend/app/api/events/service.ts +++ b/frontend/app/api/events/service.ts @@ -17,9 +17,6 @@ import { type SupabaseCredentials, } from "../utils/supabase"; -const EVENT_SELECT_FIELDS = - "id,title,description,date,start_time,end_time,location,image_url,signup_url,created_at,updated_at"; - type EventsTimeline = "upcoming" | "past"; /** diff --git a/frontend/app/events/EventCard.css b/frontend/app/events/EventCard.css index 7d7d2d4..6502187 100644 --- a/frontend/app/events/EventCard.css +++ b/frontend/app/events/EventCard.css @@ -122,7 +122,7 @@ flex-shrink: 0; } -.event-card__poster img { +.event-card__poster-image { display: block; width: 260px; height: auto; @@ -131,7 +131,7 @@ /* Responsive poster sizing */ @media (max-width: 1024px) { - .event-card__poster img { + .event-card__poster-image { width: 200px; } } @@ -141,7 +141,7 @@ justify-content: center; width: 100%; } - .event-card__poster img { + .event-card__poster-image { width: 100%; max-width: 300px; } diff --git a/frontend/app/events/EventCard.tsx b/frontend/app/events/EventCard.tsx index 42e472f..47aa1a4 100644 --- a/frontend/app/events/EventCard.tsx +++ b/frontend/app/events/EventCard.tsx @@ -1,8 +1,9 @@ import React from "react"; +import Image from "next/image"; import "./EventCard.css"; type EventCardProps = { - id: string; // id is needed for redirect to event-specfic page + id: string; // id is needed for redirect to event-specfic page monthLabel: string; weekday: string; day: string; @@ -11,12 +12,11 @@ type EventCardProps = { description: string; posterSrc?: string; posterAlt?: string; - titleLink?: string; + titleLink?: string; }; - function EventCard({ - _id, //destructure for now (prevent warnings) + id, monthLabel, weekday, day, @@ -27,6 +27,7 @@ function EventCard({ posterAlt, titleLink, }: EventCardProps) { + void id; return (
{/* Top month header */} @@ -66,12 +67,19 @@ function EventCard({ {/* Poster on the right (optional) */} {posterSrc && (
- {posterAlt + {posterAlt
)}
); -}; +} export default EventCard; diff --git a/frontend/app/events/EventSection.tsx b/frontend/app/events/EventSection.tsx index decdbcc..1f359a9 100644 --- a/frontend/app/events/EventSection.tsx +++ b/frontend/app/events/EventSection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from "react"; +import { useState } from "react"; import type { Event } from "./types"; import EventCard from "./EventCard"; import { FaRegCalendarAlt } from "react-icons/fa"; @@ -31,11 +31,12 @@ export default function EventSection({ const [pageDirection, setPageDirection] = useState(1); const shouldReduceMotion = useReducedMotion(); const totalPages = getTotalPages(events, EVENTS_PER_PAGE); - const paginatedEvents = getPaginatedEvents(events, page, EVENTS_PER_PAGE); - - useEffect(() => { - setPage((prevPage) => Math.min(prevPage, totalPages)); - }, [totalPages]); + const currentPage = Math.min(page, totalPages); + const paginatedEvents = getPaginatedEvents( + events, + currentPage, + EVENTS_PER_PAGE, + ); const pageAnimationVariants = { enter: (direction: number) => ({ @@ -53,11 +54,11 @@ export default function EventSection({ }; function updatePage(nextPage: number) { - if (nextPage < 1 || nextPage > totalPages || nextPage === page) { + if (nextPage < 1 || nextPage > totalPages || nextPage === currentPage) { return; } - setPageDirection(nextPage > page ? 1 : -1); + setPageDirection(nextPage > currentPage ? 1 : -1); setPage(nextPage); } @@ -90,7 +91,7 @@ export default function EventSection({ initial={false} > 1 && (
); } From 554eb8dca98f1d6ae416cbb4778d28ed806c1937 Mon Sep 17 00:00:00 2001 From: Krisha Date: Thu, 12 Mar 2026 00:19:43 -0400 Subject: [PATCH 8/8] fix: fix linting issues from main branch --- frontend/app/dev-preview/page.tsx | 30 +++--- frontend/components/FlipCard.jsx | 163 ++++++++++++++---------------- frontend/components/Navbar.tsx | 28 +++-- 3 files changed, 106 insertions(+), 115 deletions(-) diff --git a/frontend/app/dev-preview/page.tsx b/frontend/app/dev-preview/page.tsx index 8aaac86..37301ce 100644 --- a/frontend/app/dev-preview/page.tsx +++ b/frontend/app/dev-preview/page.tsx @@ -1,26 +1,22 @@ import AboutSection from "@/components/AboutSection"; import ExecutiveSection from "@/components/ExecutiveSection"; -import FlipCard from "@/components/FlipCard"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; export default function DevPreview() { - return ( -
+ return ( +
+
+ +
-
- -
+
+ +
-
- -
- - + - -
- -
- ); -} \ No newline at end of file +
+
+ ); +} diff --git a/frontend/components/FlipCard.jsx b/frontend/components/FlipCard.jsx index f9e154b..aa61cb2 100644 --- a/frontend/components/FlipCard.jsx +++ b/frontend/components/FlipCard.jsx @@ -2,96 +2,85 @@ import { useState } from "react"; import Image from "next/image"; -import { RotateCw } from "lucide-react"; -export default function FlipCard({ - name, - position, - description, - imageUrl -}) { - const [isFlipped, setIsFlipped] = useState(false); - const [isHovered, setIsHovered] = useState(false); +export default function FlipCard({ name, position, description, imageUrl }) { + const [isFlipped, setIsFlipped] = useState(false); + const [isHovered, setIsHovered] = useState(false); - return ( - - ); -} +
+ {/* Back side */} +
+

+ {description} +

+
+ + + + ); +} diff --git a/frontend/components/Navbar.tsx b/frontend/components/Navbar.tsx index 062fafb..aeda09b 100644 --- a/frontend/components/Navbar.tsx +++ b/frontend/components/Navbar.tsx @@ -4,18 +4,16 @@ import Image from "next/image"; import Link from "next/link"; import { useState } from "react"; +const navItems = [ + { href: "/", label: "HOME" }, + { href: "/about", label: "ABOUT" }, + { href: "/events", label: "EVENTS" }, + { href: "/contact", label: "CONTACT" }, +]; + export default function Navbar() { const [open, setOpen] = useState(false); - const NavLinks = () => ( - <> - setOpen(false)}>HOME - setOpen(false)}>ABOUT - setOpen(false)}>EVENTS - setOpen(false)}>CONTACT - - ); - return (