diff --git a/backend/controllers/eventController.js b/backend/controllers/eventController.js index f8001fe4..f9b4b2e8 100644 --- a/backend/controllers/eventController.js +++ b/backend/controllers/eventController.js @@ -55,6 +55,7 @@ export const eventList = asyncHandler(async (req, res) => { instagram_link, code, qrCode, + event_type, } = event; return { _id, @@ -68,6 +69,7 @@ export const eventList = asyncHandler(async (req, res) => { instagram_link, code, qrCode, + event_type, }; }); @@ -134,6 +136,11 @@ export const eventCreate = [ throw new Error('Instagram link must be a valid URL.'); }), + body('event_type') + .optional() + .customSanitizer((val) => val.trim()) + .isIn(['General', 'Dev', 'Open Source', 'Innovate']) + .withMessage('Event type must be one of: General, Dev, Open Source, Innovate'), asyncHandler(async (req, res) => { // Extract the validation errors from a request. diff --git a/backend/models/event.js b/backend/models/event.js index 3d0036ee..6895a981 100644 --- a/backend/models/event.js +++ b/backend/models/event.js @@ -11,6 +11,12 @@ const eventSchema = new Schema({ instagram_link: String, code: { type: String }, qrCode: { type: String }, + event_type: { + type: String, + enum: ["General", "Dev", "Open Source", "Innovate"], + required: true, + default: "General", + }, }); // Export model diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6dd26a0d..b7cfdb3c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -6,7 +6,7 @@ import Home from './components/Home/Home'; import NavBar from './components/NavBar/NavBar'; import Footer from './components/Footer/Footer'; import About from './components/About/About'; -import Events from './components/Events/Events'; +import Events from './components/NewEvents/Events'; import Opportunities from './components/Opportunities/Opportunities'; import Membership from './components/Membership/Membership'; import Login from './components/Login/Login'; diff --git a/frontend/src/components/NewEvents/EventCard.tsx b/frontend/src/components/NewEvents/EventCard.tsx new file mode 100644 index 00000000..c274c9e0 --- /dev/null +++ b/frontend/src/components/NewEvents/EventCard.tsx @@ -0,0 +1,91 @@ +import React from "react"; +import { Typography, Box } from "@mui/material"; + +type EventCardProps = { + title: string; + startDate: string; + endDate: string; + location: string; + calendar_link: string; + description: string; + instagram_link: string; + _id: string; +}; + +const EventCard = ({ + title, + startDate, + endDate, + location, + calendar_link, + description, + instagram_link, + _id, +}: EventCardProps) => { + const start = new Date(startDate); + const end = new Date(endDate); + + const formattedDate = start.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }); + + const formattedTime = `${start.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + })} - ${end.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "2-digit", + })}`; + + return ( + + + + {title} + + + + {formattedDate} | {formattedTime} + + + {location} + + + + + ); +}; + +export default EventCard; \ No newline at end of file diff --git a/frontend/src/components/NewEvents/Events.tsx b/frontend/src/components/NewEvents/Events.tsx new file mode 100644 index 00000000..f0b6f43a --- /dev/null +++ b/frontend/src/components/NewEvents/Events.tsx @@ -0,0 +1,394 @@ +import React, { useState, useEffect } from "react"; +import { Box, Grid, Button, Typography, IconButton, useTheme, useMediaQuery } from "@mui/material"; +import { ArrowBackIosNewRounded, ArrowForwardIosRounded } from "@mui/icons-material"; +import axios from "axios"; +import EventCard from "./EventCard"; +import { positions } from "@mui/system"; + +const categories = ["General", "Dev", "Open Source", "Innovate"]; + +const categoryColors: Record = { + General: "#EBB111", + Dev: "#5DF0C4", + "Open Source": "#64C3E3", + Innovate: "#725DF0", +}; + +interface Event { + calendar_link: string; + description: string; + end_time: string; + instagram_link: string; + location: string; + start_time: string; + title: string; + _id: string; + event_type: string; +} + +const EventsPage = () => { + const [selectedCategory, setSelectedCategory] = useState(null); + const [upcomingEvents, setUpcomingEvents] = useState([]); + const [pastEvents, setPastEvents] = useState([]); + const [currentIndex, setCurrentIndex] = useState(0); + + useEffect(() => { + const fetchEvents = async () => { + try { + const [upcomingRes, pastRes] = await Promise.all([ + axios.get(`${process.env.REACT_APP_BACKEND_URL}/api/v1/events?type=upcoming`), + axios.get(`${process.env.REACT_APP_BACKEND_URL}/api/v1/events?type=past`), + ]); + + const sortedUpcoming = upcomingRes.data.sort( + (a, b) => + new Date(a.start_time).getTime() - new Date(b.start_time).getTime() + ); + const sortedPast = pastRes.data.sort( + (a, b) => + new Date(b.start_time).getTime() - new Date(a.start_time).getTime() + ); + + setUpcomingEvents(sortedUpcoming); + setPastEvents(sortedPast); + } catch (error) { + console.error("Error fetching events:", error); + } + }; + + fetchEvents(); + }, []); + + const theme = useTheme(); + const isXs = useMediaQuery(theme.breakpoints.down("sm")); + const isSm = useMediaQuery(theme.breakpoints.between("sm", "md")); + const isMd = useMediaQuery(theme.breakpoints.between("md", "lg")); + + const VISIBLE_COUNT = isXs ? 1 : isSm ? 2 : isMd ? 3 : 4; + + const handlePrev = () => { + setCurrentIndex((prev) => + prev === 0 ? Math.max(filteredUpcomingEvents.length - VISIBLE_COUNT, 0) : prev - 1 + ); + }; + + const handleNext = () => { + setCurrentIndex((prev) => + prev >= Math.max(filteredUpcomingEvents.length - VISIBLE_COUNT, 0) ? 0 : prev + 1 + ); + }; + + const filteredUpcomingEvents = selectedCategory + ? upcomingEvents.filter((event: any) => event.event_type === selectedCategory) + : upcomingEvents; + + const filteredPastEvents = selectedCategory + ? pastEvents.filter((event: any) => event.event_type === selectedCategory) + : pastEvents; + + let visibleEvents: Event[] = []; + if (filteredUpcomingEvents.length <= VISIBLE_COUNT) { + visibleEvents = filteredUpcomingEvents; + } else { + visibleEvents = filteredUpcomingEvents.slice(currentIndex, currentIndex + VISIBLE_COUNT); + + if (visibleEvents.length < VISIBLE_COUNT) { + const wrapCount = VISIBLE_COUNT - visibleEvents.length; + visibleEvents = visibleEvents.concat(filteredUpcomingEvents.slice(0, wrapCount)); + } + } + + const [pastIndex, setPastIndex] = useState(0); + + const pastCols = isXs ? 1 : isSm ? 2 : isMd ? 3 : 4; + const pastRows = 3; + const pastPerPage = pastCols * pastRows; + + const paginatedPastEvents = []; + for (let i = 0; i < filteredPastEvents.length; i += pastPerPage) { + paginatedPastEvents.push(filteredPastEvents.slice(i, i + pastPerPage)); + } + + const handlePastPrev = () => { + setPastIndex((prev) => (prev === 0 ? paginatedPastEvents.length - 1 : prev - 1)); + }; + + const handlePastNext = () => { + setPastIndex((prev) => (prev === paginatedPastEvents.length - 1 ? 0 : prev + 1)); + }; + + return ( + + + Events + + + {/* Category Buttons */} + + {categories.map((cat) => { + const isSelected = selectedCategory === cat; + const color = categoryColors[cat]; + return ( + + ); + })} + + + {/* Upcoming Events */} + + {filteredUpcomingEvents.length > 0 ? ( + + {filteredUpcomingEvents.length > VISIBLE_COUNT && ( + + + + )} + + + {visibleEvents.map((event) => ( + + + + ))} + + + {filteredUpcomingEvents.length > VISIBLE_COUNT && ( + + + + )} + + ) : ( + + No upcoming events. + + )} + + + {/* Past Events */} + + + Past Events + + + {filteredPastEvents.length > 0 ? ( + + {paginatedPastEvents.length > 1 && ( + + + + )} + + + + {paginatedPastEvents[pastIndex].map((event) => ( + + + + ))} + + + + {paginatedPastEvents.length > 1 && ( + + + + )} + + ) : ( + + No past events. + + )} + + + ); +}; + +export default EventsPage; \ No newline at end of file