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