Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,38 @@ public ResponseEntity<List<Event>> getAllEvents() {
return ResponseEntity.ok(events);
}

Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing JavaDoc documentation for this new public endpoint. Other endpoints in this controller (e.g., lines 82-85) have documentation describing their purpose and behavior.

Add documentation following the existing pattern:

/**
 * Get the top 5 events sorted by ticket sales
 * Returns a list of events with their basic information and ticket count
 * @return List of top events with ticketsSold field
 */
@GetMapping("/top")
Suggested change
/**
* Get the top 5 events sorted by ticket sales.
* Returns a list of events with their basic information and ticket count.
* @return List of top events with ticketsSold field
*/

Copilot uses AI. Check for mistakes.
@GetMapping("/top")
public ResponseEntity<List<java.util.Map<String, Object>>> getTopEvents() {
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endpoint returns a List<Map<String, Object>> instead of using a proper DTO class. This inconsistent API design makes the response schema unclear and harder to maintain. Other endpoints in this controller use strongly-typed Event objects.

Consider creating a dedicated DTO (e.g., TopEventDTO) that explicitly defines the response structure:

public class TopEventDTO {
    private Long eventId;
    private String title;
    private String description;
    // ... other fields
    private Integer ticketsSold;
}

This provides type safety, better IDE support, and clearer API documentation.

Copilot uses AI. Check for mistakes.
// Get all events
List<Event> allEvents = eventRepository.findAll();
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The endpoint returns all events regardless of their approval status. Since this is a public endpoint (visible on the homepage), it should only display approved events to prevent showing pending or rejected events to users.

Add a filter to only include approved events:

List<Event> allEvents = eventRepository.findAll().stream()
    .filter(event -> "approved".equals(event.getStatus()))
    .collect(java.util.stream.Collectors.toList());
Suggested change
List<Event> allEvents = eventRepository.findAll();
List<Event> allEvents = eventRepository.findAll().stream()
.filter(event -> "approved".equals(event.getStatus()))
.collect(java.util.stream.Collectors.toList());

Copilot uses AI. Check for mistakes.

// Sort by ticket count (descending) and limit to top 5
List<java.util.Map<String, Object>> topEvents = allEvents.stream()
.map(event -> {
java.util.Map<String, Object> 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());
Comment on lines +46 to +62
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Potential N+1 query problem: Calling event.getTickets().size() for each event will trigger a separate query per event since tickets is lazily loaded. This could cause significant performance issues when there are many events.

Consider using a custom query with JOIN FETCH or a database-level COUNT to efficiently retrieve ticket counts:

@Query("SELECT e FROM Event e LEFT JOIN FETCH e.tickets")
List<Event> findAllWithTickets();

Or create a DTO projection query that counts tickets at the database level:

@Query("SELECT e.eventId as eventId, e.title as title, ..., COUNT(t) as ticketCount FROM Event e LEFT JOIN e.tickets t GROUP BY e.eventId")
List<EventWithTicketCount> findAllEventsWithTicketCount();

Copilot uses AI. Check for mistakes.
return eventMap;
})
.sorted((e1, e2) -> Integer.compare(
(Integer) e2.get("ticketsSold"),
(Integer) e1.get("ticketsSold")
))
.limit(5)
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The hardcoded limit of 5 events is a magic number that should be extracted as a named constant for better maintainability. If the requirement changes (e.g., to show top 10 events), the constant name makes it clear what needs to be updated.

Consider defining it at the class level:

private static final int TOP_EVENTS_LIMIT = 5;

Then use:

.limit(TOP_EVENTS_LIMIT)

Copilot uses AI. Check for mistakes.
.collect(java.util.stream.Collectors.toList());

return ResponseEntity.ok(topEvents);
}
Comment on lines +43 to +73
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The /events/top endpoint is not included in the Spring Security configuration. This endpoint should be added to the permitAll() list in SecurityConfig to allow unauthenticated access, similar to other GET event endpoints.

Add /api/events/top to line 62:

.requestMatchers(HttpMethod.GET, "/api/events", "/api/events/{id}", "/api/events/search", "/api/events/filter", "/api/events/top").permitAll()

Copilot uses AI. Check for mistakes.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please look into this @daniel-buta ? Try accessing the homepage without being logged in and tell me if you can still see the top events please.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I can see the top events when accessing the homepage without being logged in

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can see the top events without being logged in but when you try to save an event or buy a ticket, it will redirect you to log in.


@GetMapping("/{id}")
public ResponseEntity<Event> getEventById(@PathVariable Long id) {
return eventRepository.findById(id)
Expand Down
234 changes: 205 additions & 29 deletions frontend/my-react-app/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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";
Expand Down Expand Up @@ -47,6 +62,81 @@ function BlankLayout() {
function Home() {
const navigate = useNavigate();
const { user } = useAuth();
const { enqueueSnackbar } = useSnackbar();
const [topEvents, setTopEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(true);
const [savedEventIds, setSavedEventIds] = useState<Set<number>>(new Set());
const [savingEventId, setSavingEventId] = useState<number | null>(null);

useEffect(() => {
const fetchTopEvents = async () => {
try {
const events = await getTopEvents();
setTopEvents(events);
} catch (error) {
console.error('Failed to fetch top events:', error);
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling silently fails without providing user feedback. When the API call fails, the user sees no indication that something went wrong, they just see an empty events list (or potentially old data).

Consider adding user-facing error feedback:

} catch (error) {
    console.error('Failed to fetch top events:', error);
    enqueueSnackbar('Failed to load top events', { variant: 'error' });
} finally {
Suggested change
console.error('Failed to fetch top events:', error);
console.error('Failed to fetch top events:', error);
enqueueSnackbar('Failed to load top events', { variant: 'error' });

Copilot uses AI. Check for mistakes.
} finally {
setLoading(false);
}
};

fetchTopEvents();
}, []);

useEffect(() => {
const checkSavedEvents = async () => {
if (!localStorage.getItem('token')) return;

const savedIds = new Set<number>();
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
}
}
Comment on lines +91 to +100
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The N+1 loop pattern here can cause performance issues when checking many events. Each call to checkIfSaved() makes a separate API request, resulting in up to 5 sequential requests when displaying top events.

Consider batching this operation by:

  1. Creating a new backend endpoint that accepts multiple event IDs and returns which ones are saved, or
  2. Making the API calls in parallel using Promise.all():
const savedIds = new Set<number>();
const results = await Promise.all(
    topEvents.map(event => 
        checkIfSaved(event.eventID).catch(() => false)
    )
);
topEvents.forEach((event, index) => {
    if (results[index]) savedIds.add(event.eventID);
});
setSavedEventIds(savedIds);
Suggested change
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
}
}
const results = await Promise.all(
topEvents.map(event =>
checkIfSaved(event.eventID).catch(() => false)
)
);
topEvents.forEach((event, index) => {
if (results[index]) savedIds.add(event.eventID);
});

Copilot uses AI. Check for mistakes.
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 (
<>
Expand Down Expand Up @@ -101,33 +191,119 @@ function Home() {



<Box component = "section" sx = {{width: '100%', bgcolor: '#373f51', color: 'white'}}>
<br></br>
{/*
Former Logo
<a href="https://vite.dev" target="_blank">
<img src={viteLogo} className="logo" alt="Vite logo" />
</a>
<a href="https://react.dev" target="_blank">
<img src={reactLogo} className="logo react" alt="React logo" />
</a>
*/}
<Typography variant = "h3" className='smallertitle'> Top Events </Typography>
<Typography variant = "h4"> Backend for this hasn't been implemented yet! </Typography>
<img src = "src\images\samantha-gades-fIHozNWfcvs-unsplash.jpg" alt = "neat college photo!" style = {{maxWidth: '33%', maxHeight: '33%'}}></img>
<Typography variant = "h3"> Frosh Night </Typography>
<Typography variant = "body1"> 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! </Typography>
<br></br>
<img src = "src\images\swag-slayer-dd2EOQBycJY-unsplash.jpg" alt = "neat college photo!" style = {{maxWidth: '33%', maxHeight: '33%'}}></img>
<Typography variant = "h3"> DJ Night </Typography>
<Typography variant = "body1"> The EDM Club is organizing an all-night dance festival on the 22nd of October! Click for more details! </Typography>
<br></br>
<img src = "src\images\willian-justen-de-vasconcellos-_krHI5-8yA4-unsplash.jpg" alt = "neat college photo!" style = {{maxWidth: '33%', maxHeight: '33%'}}></img>
<Typography variant = "h3"> Campus Museum Tour</Typography>
<Typography variant = "body1"> Join us for a tour of the campus museum where you can browse artifacts of some of the school's greatest alumni! </Typography>
{/* With this, the "Top Events" container is forced to extend its height to contain the floated images*/}
<Box sx={{ clear: 'both' }}></Box>
<br></br>
<Box component = "section" sx = {{py: 6, px: 3, width: '100%', bgcolor: '#373f51', color: 'white'}}>
<Box sx={{ maxWidth: '1600px', mx: 'auto' }}>
<Typography variant = "h2" sx={{ mb: 4 }}> Top Events </Typography>

{loading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 4 }}>
<CircularProgress sx={{ color: 'white' }} />
</Box>
) : topEvents.length === 0 ? (
<Typography variant = "h5"> No events available yet. Check back soon! </Typography>
) : (
<Box
sx={{
display: 'grid',
gridTemplateColumns: {
xs: '1fr',
sm: 'repeat(2, 1fr)',
md: 'repeat(3, 1fr)'
},
gap: 3
}}
>
{topEvents.map((event) => (
<Card
key={event.eventID}
sx={{
height: '100%',
display: 'flex',
flexDirection: 'column',
transition: 'all 0.3s ease-in-out',
borderRadius: 3,
overflow: 'hidden',
'&:hover': {
transform: 'translateY(-8px)',
boxShadow: '0 12px 24px rgba(0,0,0,0.15)',
}
}}
>
{event.image && event.image.length > 0 && event.image[0] && (
<CardMedia
component="img"
height="220"
image={event.image[0]}
alt={event.title}
sx={{ objectFit: 'cover' }}
/>
)}
<CardContent sx={{ flexGrow: 1, p: 3 }}>
<Typography variant="h5" gutterBottom fontWeight="bold" sx={{ mb: 2 }}>
{event.title}
</Typography>
<Typography
variant="body1"
color="text.secondary"
sx={{
display: '-webkit-box',
WebkitLineClamp: 3,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
mb: 2,
minHeight: '4.5em'
}}
>
{event.description}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
📍 {event.location}
</Typography>
<Typography variant="body1" color="text.secondary" sx={{ mb: 1 }}>
🎫 {event.ticketsSold || 0} tickets sold
</Typography>
<Typography variant="h6" color="primary.main" fontWeight="bold" sx={{ mt: 2 }}>
{event.price === 0 ? 'Free' : `$${event.price}`}
</Typography>
</CardContent>
<CardActions sx={{ p: 2.5, pt: 0, gap: 1 }}>
<Button
onClick={() => handleEventClick(event.eventID)}
variant="contained"
size="medium"
fullWidth
>
Buy Ticket
</Button>
<Button
onClick={() => handleAddToFavorites(event.eventID)}
variant={savedEventIds.has(event.eventID) ? "contained" : "outlined"}
size="medium"
disabled={savingEventId === event.eventID}
fullWidth
>
{savedEventIds.has(event.eventID) ? 'Saved' : 'Save Event'}
</Button>
</CardActions>
</Card>
))}
</Box>
)}

<Box sx={{ mt: 4, textAlign: 'center' }}>
<Button
variant="contained"
size="large"
onClick={() => navigate('/events')}
sx={{
bgcolor: '#008dd5',
'&:hover': { bgcolor: '#007bbf' }
}}
>
View All Events
</Button>
</Box>
</Box>
</Box>

</>
Expand Down
21 changes: 21 additions & 0 deletions frontend/my-react-app/src/api/events.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,24 @@ export const updateEvent = async (eventId: number, eventData: any) => {
});
return response.data;
};

export const getTopEvents = async (): Promise<Event[]> => {
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}`]) : [],
Copy link

Copilot AI Nov 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image URL handling is inconsistent with other functions in this file. The getTopEvents function includes logic to check if the URL starts with 'http' or 'https', but other functions like getAllEvents() (line 13) and getEventById() (line 33) simply prepend http://localhost:8080 without this check.

For consistency, either:

  1. Apply the same URL handling logic across all functions, or
  2. Simplify this to match the existing pattern if all image URLs are expected to be relative paths:
image: event.imageUrl ? [`http://localhost:8080${event.imageUrl}`] : [],

Copilot uses AI. Check for mistakes.
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;
};
Loading