diff --git a/frontend/app/api/admin-login/route.ts b/frontend/app/api/admin-login/route.ts index 3c2d324..04e619b 100644 --- a/frontend/app/api/admin-login/route.ts +++ b/frontend/app/api/admin-login/route.ts @@ -9,7 +9,7 @@ export async function POST(req: NextRequest) { method: 'POST', headers: { 'Content-Type': 'application/json', - 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY}`, + 'Authorization': `Bearer ${process.env.SUPABASE_ANON_KEY}`, }, body: JSON.stringify({ email, password }), } diff --git a/frontend/app/events/[id]/page.tsx b/frontend/app/events/[id]/page.tsx index c7388be..96e8959 100644 --- a/frontend/app/events/[id]/page.tsx +++ b/frontend/app/events/[id]/page.tsx @@ -1,141 +1,192 @@ -import Image from "next/image"; +/** + * Event Detail Page + * + * Purpose: + * - Displays detailed information for a specific event + * - Fetches event data from API based on URL parameter + * - Shows event description, venue map, and registration link + * + * Notes: + * - Route: /events/{id} where id is the event.id + * - Fetches data client-side via /api/events/:id + */ + +"use client"; + import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; import { MapPin } from "lucide-react"; import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; - -type EventData = { - id: string; - monthLabel: string; - weekday: string; - day: string; - meta: string; - title: string; - description: string; - posterSrc?: string; - posterAlt?: string; - titleLink?: string; +import ErrorState from "@/components/ErrorState"; +import EventImage from "@/app/events/components/EventImage"; +import type { Event } from "@/app/events/types"; +import { getDateParts } from "@/app/events/utils"; + +type EventResponse = { + event?: Event; + error?: string; }; -const EVENT_DATA: Record = { - "amr-youth-policy": { - id: "amr-youth-policy", - monthLabel: "SEPTEMBER 2025", - weekday: "MON", - day: "25", - meta: "September 25 Location: IC Atrium @ 3:00 pm - 8:00 pm", - title: "Event title", - description: - "Lorem ipsum dolor sit amet consectetur. Odio sit congue euismod viverra magna.\n\nMi consequat mattis urna condimentum orci. Urna neque magna massa bibendum. Erat pharetra nulla euismod in fames tellus eu.", - posterSrc: undefined, - posterAlt: "Event poster", - titleLink: "#", - }, -}; +async function fetchEventById(eventId: string): Promise { + const response = await fetch(`/api/events/${eventId}`); + const data = (await response + .json() + .catch(() => null)) as EventResponse | null; -const FALLBACK_EVENT: EventData = { - id: "fallback", - monthLabel: "SEPTEMBER 2025", - weekday: "MON", - day: "25", - meta: "September 25 Location: IC Atrium @ 3:00 pm - 8:00 pm", - title: "Event title", - description: - "Lorem ipsum dolor sit amet consectetur. Odio sit congue euismod viverra magna.\n\nMi consequat mattis urna condimentum orci. Urna neque magna massa bibendum. Erat pharetra nulla euismod in fames tellus eu.", - posterSrc: undefined, - posterAlt: "Event poster", - titleLink: "#", -}; + if (!response.ok || !data?.event) { + throw new Error(data?.error || "Failed to fetch event"); + } -function getLocation(meta: string) { - const match = meta.match(/Location:\s*(.*?)\s*@/i); - return match?.[1]?.trim() || "location"; + return data.event; } -function getDateTimeLabel(event: EventData) { - return `${event.weekday} ${event.day} - ${event.meta}`; +function getLocation(location: string | null): string { + return location?.trim() || "University of Toronto Scarborough"; } -function EventImage({ src, alt }: { src?: string; alt: string }) { - if (src) { +export default function EventDetailPage() { + const params = useParams(); + const eventId = Array.isArray(params?.id) ? params?.id[0] : params?.id; + const [event, setEvent] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + //use effect for client side loading event using id + useEffect(() => { + if (!eventId) { + setError("Missing event id"); + setLoading(false); + return; + } + + let isMounted = true; + + async function loadEvent() { + try { + const eventData = await fetchEventById(eventId); + if (isMounted) { + setEvent(eventData); + setError(null); + } + } catch (err) { + if (isMounted) { + setError(err instanceof Error ? err.message : "Failed to load event"); + } + } finally { + if (isMounted) { + setLoading(false); + } + } + } + + setLoading(true); + loadEvent(); + + return () => { + isMounted = false; + }; + }, [eventId]); + + if (loading) { return ( - {alt} +
+
+ +
+
+ Loading event... +
+
+
+
+
); } - return ( -
- ); -} + if (error || !event) { + return ( +
+
+ +
+ +
+ + Back to Events + +
+
+
+
+
+ ); + } -export default async function EventDetailPage({ - params, -}: { - params: Promise<{ id: string }>; -}) { - const { id } = await params; - const event = EVENT_DATA[id] ?? FALLBACK_EVENT; - const locationLabel = getLocation(event.meta); - const mapSrc = `https://www.google.com/maps?q=${encodeURIComponent( - locationLabel === "location" - ? "University of Toronto Scarborough" - : locationLabel, - )}&output=embed`; + const { monthLabel, weekday, day } = getDateParts(event.date); + const location = getLocation(event.location); + const dateTimeLabel = `${weekday} ${day} - ${monthLabel} @ ${event.start_time || "TBA"}`; + const mapSrc = `https://www.google.com/maps?q=${encodeURIComponent(location)}&output=embed`; + const posterSrc = event.image_url || undefined; + //successful path - event detail page return (
+ + {/* main title */}

{event.title}

- {getDateTimeLabel(event)} + {dateTimeLabel}

+ {/* event details - image, desc, location and registration */}
- +
-
+

Description

-

- {event.description} +

+ {event.description || + "No description available for this event."}

- -

- {locationLabel} + +

+ {location}

- - Register now! - + {event.signup_url && ( + + Register now! + + )}
@@ -144,7 +195,7 @@ export default async function EventDetailPage({ Venue

- {locationLabel} + {location}

diff --git a/frontend/app/events/EventCard.css b/frontend/app/events/components/EventCard.css similarity index 100% rename from frontend/app/events/EventCard.css rename to frontend/app/events/components/EventCard.css diff --git a/frontend/app/events/EventCard.tsx b/frontend/app/events/components/EventCard.tsx similarity index 51% rename from frontend/app/events/EventCard.tsx rename to frontend/app/events/components/EventCard.tsx index 47aa1a4..ed2e3b5 100644 --- a/frontend/app/events/EventCard.tsx +++ b/frontend/app/events/components/EventCard.tsx @@ -1,35 +1,45 @@ +/** + * EventCard Component + * + * Purpose: + * - Displays event information in a card format + * - Provides clickable navigation to event detail page + * - Formats date information from ISO date string + * + * Notes: + * - Automatically computes date parts (monthLabel, weekday, day) from event.date + * - Links to `/events/{id}` for event detail view + */ + +"use client"; + import React from "react"; import Image from "next/image"; +import Link from "next/link"; +import type { Event } from "@/app/events/types"; +import { getDateParts } from "@/app/events/utils"; import "./EventCard.css"; -type EventCardProps = { - id: string; // id is needed for redirect to event-specfic page - monthLabel: string; - weekday: string; - day: string; - meta: string; - title: string; - description: string; - posterSrc?: string; - posterAlt?: string; - titleLink?: string; -}; +type EventCardProps = Event; function EventCard({ id, - monthLabel, - weekday, - day, - meta, title, description, - posterSrc, - posterAlt, - titleLink, + date, + location, + image_url, + signup_url, }: EventCardProps) { - void id; + const { monthLabel, weekday, day } = getDateParts(date); + const meta = location ? `${date} Location: ${location}` : date; + const posterSrc = image_url || "/globe.svg"; + return ( -
+ {/* Top month header */}
{monthLabel} @@ -48,20 +58,25 @@ function EventCard({

{meta}

- {titleLink ? ( - { + e.preventDefault(); + e.stopPropagation(); + window.open(signup_url, "_blank", "noopener,noreferrer"); + }} > {title} - + ) : (

{title}

)} -

{description}

+

+ {description || ""} +

{/* Poster on the right (optional) */} @@ -69,7 +84,7 @@ function EventCard({
{posterAlt )}
-
+ ); } diff --git a/frontend/app/events/components/EventImage.tsx b/frontend/app/events/components/EventImage.tsx new file mode 100644 index 0000000..2aa803a --- /dev/null +++ b/frontend/app/events/components/EventImage.tsx @@ -0,0 +1,39 @@ +/** + * EventImage Component + * + * Purpose: + * - Displays event poster image or fallback placeholder + * - Handles responsive image sizing with Next.js Image optimization + * + * Notes: + * - Uses Next.js Image component for optimization when src is provided + * - Falls back to a decorative gradient placeholder when no src available + */ + +import Image from "next/image"; + +type EventImageProps = { + src?: string; + alt: string; +}; + +export default function EventImage({ src, alt }: EventImageProps) { + if (src) { + return ( + {alt} + ); + } + + return ( +
+ ); +} diff --git a/frontend/app/events/EventSection.tsx b/frontend/app/events/components/EventSection.tsx similarity index 81% rename from frontend/app/events/EventSection.tsx rename to frontend/app/events/components/EventSection.tsx index 1f359a9..ceb66dc 100644 --- a/frontend/app/events/EventSection.tsx +++ b/frontend/app/events/components/EventSection.tsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import type { Event } from "./types"; -import EventCard from "./EventCard"; +import type { Event } from "@/app/events/types"; +import EventCard from "@/app/events/components/EventCard"; import { FaRegCalendarAlt } from "react-icons/fa"; import { AnimatePresence, @@ -11,10 +11,9 @@ import { } from "framer-motion"; import { EVENTS_PER_PAGE, - getDateParts, getPaginatedEvents, getTotalPages, -} from "./utils"; +} from "@/app/events/utils"; type EventSectionProps = { isCurrent: boolean; @@ -38,6 +37,7 @@ export default function EventSection({ EVENTS_PER_PAGE, ); + //page animation and pagination effects const pageAnimationVariants = { enter: (direction: number) => ({ opacity: 0, @@ -103,30 +103,9 @@ export default function EventSection({ ease: "easeOut", }} > - {paginatedEvents.map((event) => { - const { monthLabel, weekday, day } = getDateParts( - event.date, - ); - - return ( - - ); - })} + {paginatedEvents.map((event) => ( + + ))} diff --git a/frontend/app/events/page.tsx b/frontend/app/events/page.tsx index 5088e4f..964fd77 100644 --- a/frontend/app/events/page.tsx +++ b/frontend/app/events/page.tsx @@ -2,9 +2,9 @@ import Navbar from "@/components/Navbar"; import Footer from "@/components/Footer"; import ErrorState from "@/components/ErrorState"; -import EventSection from "./EventSection"; +import EventSection from "@/app/events/components/EventSection"; import { useEffect, useState } from "react"; -import type { Event, EventsApiResponse } from "./types"; +import type { Event, EventsApiResponse } from "@/app/events/types"; async function fetchEventBucket(endpoint: string): Promise { const response = await fetch(endpoint); diff --git a/frontend/next.config.ts b/frontend/next.config.ts index e9ffa30..aa68556 100644 --- a/frontend/next.config.ts +++ b/frontend/next.config.ts @@ -1,7 +1,15 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "uhugvwdetrwvhhrsyvoz.supabase.co", + pathname: "/storage/v1/object/public/event-images/**", + }, + ], + }, }; export default nextConfig;