Conversation
There was a problem hiding this comment.
Pull Request Overview
This PR implements a dynamic "Top Events" section on the homepage that displays the 5 most popular events based on ticket sales, enhancing user engagement by showcasing trending campus events.
Key Changes:
- Added backend
/api/events/topendpoint that returns events sorted by ticket count - Created responsive card grid UI with event cards showing image, title, description, location, price, and tickets sold
- Integrated authentication-aware features for buying tickets and saving events with proper error handling
Reviewed Changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 9 comments.
| File | Description |
|---|---|
| backend/springboot-app/src/main/java/com/linkt/controller/EventController.java | Added new /top GET endpoint to retrieve the 5 most popular events sorted by ticket sales |
| frontend/my-react-app/src/api/events.api.ts | Added getTopEvents() API function with data transformation to match frontend Event interface |
| frontend/my-react-app/src/App.tsx | Replaced static placeholder content with dynamic top events display, including loading states, authentication checks, and save/buy functionality |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @GetMapping("/top") | ||
| public ResponseEntity<List<java.util.Map<String, Object>>> getTopEvents() { | ||
| // Get all events | ||
| List<Event> allEvents = eventRepository.findAll(); | ||
|
|
||
| // 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()); | ||
| 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); | ||
| } |
There was a problem hiding this comment.
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()There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Yes, I can see the top events when accessing the homepage without being logged in
There was a problem hiding this comment.
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.
| 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}`]) : [], |
There was a problem hiding this comment.
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:
- Apply the same URL handling logic across all functions, or
- 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}`] : [],| 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 | ||
| } | ||
| } |
There was a problem hiding this comment.
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:
- Creating a new backend endpoint that accepts multiple event IDs and returns which ones are saved, or
- 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);| 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); | |
| }); |
| @@ -40,6 +40,38 @@ public ResponseEntity<List<Event>> getAllEvents() { | |||
| return ResponseEntity.ok(events); | |||
| } | |||
|
|
|||
There was a problem hiding this comment.
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")| /** | |
| * 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 | |
| */ |
| const events = await getTopEvents(); | ||
| setTopEvents(events); | ||
| } catch (error) { | ||
| console.error('Failed to fetch top events:', error); |
There was a problem hiding this comment.
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 {| console.error('Failed to fetch top events:', error); | |
| console.error('Failed to fetch top events:', error); | |
| enqueueSnackbar('Failed to load top events', { variant: 'error' }); |
| (Integer) e2.get("ticketsSold"), | ||
| (Integer) e1.get("ticketsSold") | ||
| )) | ||
| .limit(5) |
There was a problem hiding this comment.
[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)| List<Event> allEvents = eventRepository.findAll(); | ||
|
|
||
| // 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()); |
There was a problem hiding this comment.
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();| @GetMapping("/top") | ||
| public ResponseEntity<List<java.util.Map<String, Object>>> getTopEvents() { | ||
| // Get all events | ||
| List<Event> allEvents = eventRepository.findAll(); |
There was a problem hiding this comment.
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());| List<Event> allEvents = eventRepository.findAll(); | |
| List<Event> allEvents = eventRepository.findAll().stream() | |
| .filter(event -> "approved".equals(event.getStatus())) | |
| .collect(java.util.stream.Collectors.toList()); |
| } | ||
|
|
||
| @GetMapping("/top") | ||
| public ResponseEntity<List<java.util.Map<String, Object>>> getTopEvents() { |
There was a problem hiding this comment.
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.
DudeNamedDarcy
left a comment
There was a problem hiding this comment.
Looked great during the meeting today! Thanks for implementing it!
Implemented a dynamic Top Events section on the homepage that displays the 5 most popular events based on ticket sales.
Backend:
Frontend:
closes #139