diff --git a/backend/springboot-app/src/main/java/com/linkt/controller/EventController.java b/backend/springboot-app/src/main/java/com/linkt/controller/EventController.java index eeedec0..4f0a46b 100644 --- a/backend/springboot-app/src/main/java/com/linkt/controller/EventController.java +++ b/backend/springboot-app/src/main/java/com/linkt/controller/EventController.java @@ -40,6 +40,38 @@ public ResponseEntity> getAllEvents() { return ResponseEntity.ok(events); } + @GetMapping("/top") + public ResponseEntity>> getTopEvents() { + // Get all events + List allEvents = eventRepository.findAll(); + + // Sort by ticket count (descending) and limit to top 5 + List> topEvents = allEvents.stream() + .map(event -> { + java.util.Map eventMap = new java.util.HashMap<>(); + eventMap.put("eventId", event.getEventId()); + eventMap.put("title", event.getTitle()); + eventMap.put("description", event.getDescription()); + eventMap.put("eventType", event.getEventType()); + eventMap.put("startDateTime", event.getStartDateTime()); + eventMap.put("endDateTime", event.getEndDateTime()); + eventMap.put("location", event.getLocation()); + eventMap.put("capacity", event.getCapacity()); + eventMap.put("imageUrl", event.getImageUrl()); + eventMap.put("price", event.getPrice()); + eventMap.put("ticketsSold", event.getTickets().size()); + return eventMap; + }) + .sorted((e1, e2) -> Integer.compare( + (Integer) e2.get("ticketsSold"), + (Integer) e1.get("ticketsSold") + )) + .limit(5) + .collect(java.util.stream.Collectors.toList()); + + return ResponseEntity.ok(topEvents); + } + @GetMapping("/{id}") public ResponseEntity getEventById(@PathVariable Long id) { return eventRepository.findById(id) diff --git a/frontend/my-react-app/src/App.tsx b/frontend/my-react-app/src/App.tsx index 1e99198..05e9b6d 100644 --- a/frontend/my-react-app/src/App.tsx +++ b/frontend/my-react-app/src/App.tsx @@ -1,8 +1,19 @@ // src/App.tsx import {Routes, Route, useNavigate, Outlet, Navigate} from 'react-router-dom'; import { useAuth } from './contexts/AuthContext'; -import { Toolbar, Box, Typography } from "@mui/material"; - +import { useState, useEffect } from 'react'; +import { + Toolbar, + Box, + Typography, + Card, + CardMedia, + CardContent, + CardActions, + Button, + CircularProgress, +} from "@mui/material"; +//import '@fontsource-variable/cabin'; import './App.css'; import SignUp from './SignUp'; import Login from './Login'; @@ -20,6 +31,10 @@ import MyEventsPage from "./pages/MyEventsPage.tsx"; import EditEventPage from "./pages/EditEventPage.tsx"; import ScanTicketPage from "./pages/ScanTicketPage.tsx"; import AdminDashboard from "./pages/AdminDashboard.tsx"; +import { getTopEvents } from './api/events.api'; +import { saveEvent, checkIfSaved } from './api/savedEvents.api'; +import type { Event } from './types/event.interface'; +import { useSnackbar } from 'notistack'; import ApproveEventsPage from "./pages/ApproveEventsPage.tsx"; import EmailVerificationPage from "./pages/EmailVerificationPage.tsx"; import TwoFactorAuthPage from "./pages/TwoFactorAuthPage.tsx"; @@ -47,6 +62,81 @@ function BlankLayout() { function Home() { const navigate = useNavigate(); const { user } = useAuth(); + const { enqueueSnackbar } = useSnackbar(); + const [topEvents, setTopEvents] = useState([]); + const [loading, setLoading] = useState(true); + const [savedEventIds, setSavedEventIds] = useState>(new Set()); + const [savingEventId, setSavingEventId] = useState(null); + + useEffect(() => { + const fetchTopEvents = async () => { + try { + const events = await getTopEvents(); + setTopEvents(events); + } catch (error) { + console.error('Failed to fetch top events:', error); + } finally { + setLoading(false); + } + }; + + fetchTopEvents(); + }, []); + + useEffect(() => { + const checkSavedEvents = async () => { + if (!localStorage.getItem('token')) return; + + const savedIds = new Set(); + for (const event of topEvents) { + try { + const isSaved = await checkIfSaved(event.eventID); + if (isSaved) { + savedIds.add(event.eventID); + } + } catch (error) { + // Ignore errors for individual checks + } + } + setSavedEventIds(savedIds); + }; + + if (topEvents.length > 0) { + checkSavedEvents(); + } + }, [topEvents]); + + const handleAddToFavorites = async (eventId: number) => { + if (!localStorage.getItem('token')) { + navigate('/login'); + return; + } + + setSavingEventId(eventId); + try { + await saveEvent(eventId); + setSavedEventIds(prev => new Set(prev).add(eventId)); + enqueueSnackbar('Event saved!', { variant: 'success' }); + } catch (error: any) { + if (error.response?.status === 401) { + navigate('/login'); + } else { + enqueueSnackbar('Failed to save event', { variant: 'error' }); + } + } finally { + setSavingEventId(null); + } + }; + + const handleEventClick = (eventId: number) => { + if (!user) { + // User not logged in, redirect to login + navigate('/login'); + } else { + // User is logged in, proceed to checkout + navigate(`/checkout/${eventId}`); + } + }; return ( <> @@ -101,33 +191,119 @@ function Home() { - -

- {/* - Former Logo - - Vite logo - - - React logo - - */} - Top Events - Backend for this hasn't been implemented yet! - neat college photo! - Frosh Night - New to school and don't know where to start? Have some drinks, play games and meet some new people at the school's frosh night! -

- neat college photo! - DJ Night - The EDM Club is organizing an all-night dance festival on the 22nd of October! Click for more details! -

- neat college photo! - Campus Museum Tour - Join us for a tour of the campus museum where you can browse artifacts of some of the school's greatest alumni! - {/* With this, the "Top Events" container is forced to extend its height to contain the floated images*/} - -

+ + + Top Events + + {loading ? ( + + + + ) : topEvents.length === 0 ? ( + No events available yet. Check back soon! + ) : ( + + {topEvents.map((event) => ( + + {event.image && event.image.length > 0 && event.image[0] && ( + + )} + + + {event.title} + + + {event.description} + + + 📍 {event.location} + + + 🎫 {event.ticketsSold || 0} tickets sold + + + {event.price === 0 ? 'Free' : `$${event.price}`} + + + + + + + + ))} + + )} + + + + + diff --git a/frontend/my-react-app/src/api/events.api.ts b/frontend/my-react-app/src/api/events.api.ts index 4797e33..a8686a6 100644 --- a/frontend/my-react-app/src/api/events.api.ts +++ b/frontend/my-react-app/src/api/events.api.ts @@ -76,3 +76,24 @@ export const updateEvent = async (eventId: number, eventData: any) => { }); return response.data; }; + +export const getTopEvents = async (): Promise => { + const response = await axiosInstance.get('/events/top'); + + // Transform backend data to match frontend interface + const transformedEvents: Event[] = response.data.map((event: any) => ({ + eventID: event.eventId, + title: event.title, + description: event.description, + category: event.eventType, + image: event.imageUrl ? (event.imageUrl.startsWith('http') || event.imageUrl.startsWith('https') ? [event.imageUrl] : [`http://localhost:8080${event.imageUrl}`]) : [], + price: event.price || 0, + startDate: new Date(event.startDateTime), + endDate: new Date(event.endDateTime), + location: event.location, + capacity: event.capacity, + ticketsSold: event.ticketsSold || 0, + })); + + return transformedEvents; +};