diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/controllers/EventsController.java b/backend/sportsmatch/src/main/java/com/sportsmatch/controllers/EventsController.java index fec05a1f..c0652882 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/controllers/EventsController.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/controllers/EventsController.java @@ -6,6 +6,7 @@ import com.sportsmatch.models.Event; import com.sportsmatch.services.EventService; import jakarta.validation.Valid; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Pageable; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -56,21 +57,22 @@ public ResponseEntity getUpcomingEvents(@RequestBody List sportsIds) { * @return a list of finished EventHistoryDTO of the logged-in user */ @GetMapping("/event-history") - public List getEventsHistory(final Pageable pageable) { + public List getEventsHistory(@ParameterObject final Pageable pageable) { return eventService.getEventsHistory(pageable); } - /** - * This endpoint returns list of Events sorted by distance from the given location. User can filter by sports. + * This endpoint returns list of Events sorted by distance from the given location. User can + * filter by sports. * - * @param requestEventDTO it contains longitude and latitude and a list of sports for filter if given - * @param pageable it contains the page and size for pagination + * @param requestEventDTO it contains longitude and latitude and a list of sports for filter if + * given + * @param pageable it contains the page and size for pagination * @return a list of Events sorted by distance from the given location. User can filter by sports. */ @GetMapping("/nearby") - public List getNearbyEvents(@RequestBody RequestEventDTO requestEventDTO, - final Pageable pageable) { + public List getNearbyEvents( + @ParameterObject RequestEventDTO requestEventDTO, @ParameterObject final Pageable pageable) { return eventService.getNearbyEvents(requestEventDTO, pageable); } @@ -83,4 +85,14 @@ public ResponseEntity joinEvent(@PathVariable("id") Long id) { return ResponseEntity.badRequest().body(e.getMessage()); } } + + /** + * This endpoint returns the upcoming matches of the logged-in user. + * + * @return a list of logged-in user's upcoming EventDTOs ordered by date ascending + */ + @GetMapping("/upcoming-matches") + public List getUpcomingMatches() { + return eventService.getUsersUpcomingEvents(); + } } diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/controllers/SportController.java b/backend/sportsmatch/src/main/java/com/sportsmatch/controllers/SportController.java index 58eea7e8..98299148 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/controllers/SportController.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/controllers/SportController.java @@ -3,6 +3,7 @@ import com.sportsmatch.dtos.SportDTO; import com.sportsmatch.services.SportService; import lombok.RequiredArgsConstructor; +import org.springdoc.core.annotations.ParameterObject; import org.springframework.data.domain.Pageable; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; @@ -24,7 +25,7 @@ public class SportController { * @return paginated list of SportDTO */ @GetMapping("/all") - public List getSports(final Pageable pageable) { + public List getSports(@ParameterObject final Pageable pageable) { return sportService.getAllSports(pageable); } } diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/EventDTO.java b/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/EventDTO.java index c04f925f..7309ddb0 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/EventDTO.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/EventDTO.java @@ -19,8 +19,6 @@ public class EventDTO { private LocalDateTime dateStart; @NotNull private LocalDateTime dateEnd; - @NotBlank - private String location; @NotNull private Integer minElo; @NotNull diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/SportDTO.java b/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/SportDTO.java index a073448a..c00266f4 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/SportDTO.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/dtos/SportDTO.java @@ -13,10 +13,12 @@ public class SportDTO { public String emoji; public String backgroundUImageURL; + public Long id; - public SportDTO(String name, String emoji, String backgroundUImageURL) { + public SportDTO(String name, String emoji, String backgroundUImageURL, Long id) { this.name = name; this.emoji = emoji; this.backgroundUImageURL = backgroundUImageURL; + this.id = id; } } diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/mappers/SportMapper.java b/backend/sportsmatch/src/main/java/com/sportsmatch/mappers/SportMapper.java index b14e50fe..1191d986 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/mappers/SportMapper.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/mappers/SportMapper.java @@ -18,14 +18,15 @@ public SportDTO toDTO(Sport entity) { .name(entity.getName()) .emoji(entity.getEmoji()) .backgroundUImageURL(entity.getBackgroundImageURL()) + .id(entity.getId()) .build(); } public Sport toEntity(SportDTO sportDTO) { return Sport.builder() - .name(sportDTO.getName()) - .emoji(sportDTO.getEmoji()) - .backgroundImageURL(sportDTO.getBackgroundUImageURL()) - .build(); + .name(sportDTO.getName()) + .emoji(sportDTO.getEmoji()) + .backgroundImageURL(sportDTO.getBackgroundUImageURL()) + .build(); } } diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/models/Event.java b/backend/sportsmatch/src/main/java/com/sportsmatch/models/Event.java index c6f6ef4a..a36e6577 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/models/Event.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/models/Event.java @@ -16,9 +16,7 @@ @AllArgsConstructor public class Event { - @Id - @GeneratedValue - private Long id; + @Id @GeneratedValue private Long id; @Column(name = "date_start") private LocalDateTime dateStart; @@ -34,6 +32,7 @@ public class Event { private String title; + @Column(name = "is_rank_updated") private Boolean isRanksUpdated = false; @OneToMany(cascade = CascadeType.ALL, mappedBy = "event") @@ -42,8 +41,7 @@ public class Event { @OneToMany(cascade = CascadeType.ALL, mappedBy = "event") private Set ratings = new HashSet<>(); - @ManyToOne - private Sport sport; + @ManyToOne private Sport sport; @ManyToOne @JoinColumn(name = "place_id") diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/repositories/EventRepository.java b/backend/sportsmatch/src/main/java/com/sportsmatch/repositories/EventRepository.java index 2219ad85..d933af22 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/repositories/EventRepository.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/repositories/EventRepository.java @@ -21,18 +21,20 @@ public interface EventRepository extends JpaRepository { /** * Retrieves events filtered by user and finished status. * - * @param name the logged user's name to filter events by - * @param now the current time to filter events by + * @param id the logged user's id to filter events by + * @param now the current time to filter events by * @param pageable pagination information (page, size) * @return a list of events filtered by user and finished status */ // =?1 =?2") - @Query("SELECT ep.event FROM EventPlayer ep WHERE ep.player.name = :name AND ep.event.dateEnd < :now") + @Query("SELECT ep.event FROM EventPlayer ep WHERE ep.player.id = :id AND ep.event.dateEnd < :now") List findEventsByUser( - @Param("name") String name, - @Param("now") LocalDateTime now, - Pageable pageable - ); + @Param("id") Long id, @Param("now") LocalDateTime now, Pageable pageable); + + @Query("SELECT ep.event FROM EventPlayer ep WHERE ep.player.id = :id AND ep.event.dateStart > :now ORDER BY ep.event.dateEnd ASC") + List findUpcomingEventsByUser(@Param("id") Long id, @Param("now") LocalDateTime now); + + /** @@ -49,7 +51,7 @@ List findEventsByUser( * @return list of events filtered by sport names if given, and order by distance from the user's given location */ @Query(nativeQuery = true, value = - "SELECT e.id, e.date_start, e.date_end, e.min_elo, e.max_elo, e.title, e.sport_id, e.place_id " + "SELECT e.id, e.date_start, e.date_end, e.min_elo, e.max_elo, e.title, e.is_rank_updated, e.sport_id, e.place_id " + "FROM events e " + "JOIN sports s ON e.sport_id = s.id " + "JOIN places p ON e.place_id = p.id " diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/services/EventService.java b/backend/sportsmatch/src/main/java/com/sportsmatch/services/EventService.java index 9c5f90b2..a92072be 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/services/EventService.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/services/EventService.java @@ -111,8 +111,9 @@ public void deleteEventFromDatabase(Event eventById) { */ public List getEventsHistory(final Pageable pageable) { String loggedUserName = userService.getUserFromContext().getName(); + Long loggedUserId = userService.getUserFromContext().getId(); - return eventRepository.findEventsByUser(loggedUserName, LocalDateTime.now(), pageable).stream() + return eventRepository.findEventsByUser(loggedUserId, LocalDateTime.now(), pageable).stream() .map(event -> eventMapper.toDTO(event, loggedUserName, checkScoreMatch(event.getPlayers()))) .collect(Collectors.toList()); } @@ -201,4 +202,18 @@ public List getNearbyEvents(RequestEventDTO requestEventDTO, final Pag return events.stream().map(eventMapper::convertEventToEventDTO).collect(Collectors.toList()); } + + /** + * Returns the upcoming matches of the logged-in user. + * + * @return a list of logged-in user's upcoming EventDTOs ordered by date ascending + */ + public List getUsersUpcomingEvents() { + User loggedUser = userService.getUserFromContext(); + return eventRepository + .findUpcomingEventsByUser(loggedUser.getId(), LocalDateTime.now()) + .stream() + .map(eventMapper::convertEventToEventDTO) + .collect(Collectors.toList()); + } } diff --git a/backend/sportsmatch/src/main/java/com/sportsmatch/services/UserServiceImp.java b/backend/sportsmatch/src/main/java/com/sportsmatch/services/UserServiceImp.java index f0c838b4..61ffbd53 100644 --- a/backend/sportsmatch/src/main/java/com/sportsmatch/services/UserServiceImp.java +++ b/backend/sportsmatch/src/main/java/com/sportsmatch/services/UserServiceImp.java @@ -1,13 +1,13 @@ package com.sportsmatch.services; +import com.sportsmatch.dtos.SportDTO; import com.sportsmatch.dtos.UserDTO; +import com.sportsmatch.dtos.UserInfoDTO; import com.sportsmatch.mappers.SportMapper; import com.sportsmatch.mappers.UserMapper; import com.sportsmatch.models.*; import com.sportsmatch.repositories.SportRepository; import com.sportsmatch.repositories.UserRepository; -import com.sportsmatch.dtos.SportDTO; -import com.sportsmatch.dtos.UserInfoDTO; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -16,13 +16,11 @@ import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import org.springframework.web.server.ResponseStatusException; + import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; +import java.util.*; @Service @RequiredArgsConstructor @@ -69,16 +67,21 @@ public UserDTO getUserById(Long id) { throw new ResponseStatusException(HttpStatus.NOT_FOUND); } + Set sportUsers = user.get().getSportUsers(); + + List userSports = sportUsers.stream() + .map(SportUser::getSport) + .map(sportMapper::toDTO) + .toList(); + List events = new ArrayList<>(user.get().getEventsPlayed()); - List sports = new ArrayList<>(); for (EventPlayer e : events) { rankService.updatePlayersRanks(e.getEvent()); - sports.add(sportMapper.toDTO(e.getEvent().getSport())); } return UserDTO.builder() .name(user.get().getName()) - .sports(sports) + .sports(userSports) .elo(user.get().getRank()) .win(user.get().getWin()) .loss(user.get().getLoss()) diff --git a/backend/sportsmatch/src/test/java/com/sportsmatch/services/SportServiceImpTest.java b/backend/sportsmatch/src/test/java/com/sportsmatch/services/SportServiceImpTest.java index 6ac04b25..509d9dca 100644 --- a/backend/sportsmatch/src/test/java/com/sportsmatch/services/SportServiceImpTest.java +++ b/backend/sportsmatch/src/test/java/com/sportsmatch/services/SportServiceImpTest.java @@ -5,7 +5,6 @@ import com.sportsmatch.mappers.SportMapper; import com.sportsmatch.models.Sport; import com.sportsmatch.repositories.SportRepository; -import com.sportsmatch.services.SportServiceImp; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; @@ -46,8 +45,8 @@ void getAllSportsShouldReturnAllSportsWhenRequired() { List sports = Arrays.asList(sport1, sport2); Page sportsPage = new PageImpl<>(sports, pageable, sports.size()); - SportDTO sportDTO1 = new SportDTO("Football", footballEmoji, "urlFootball"); - SportDTO sportDTO2 = new SportDTO("Basketball", basketballEmoji, "urlBasketball"); + SportDTO sportDTO1 = new SportDTO("Football", footballEmoji, "urlFootball", 1L); + SportDTO sportDTO2 = new SportDTO("Basketball", basketballEmoji, "urlBasketball", 2L); when(sportMapper.toDTO(sport1)).thenReturn(sportDTO1); when(sportMapper.toDTO(sport2)).thenReturn(sportDTO2); diff --git a/backend/sportsmatch/src/test/java/com/sportsmatch/services/UserServiceImpTest.java b/backend/sportsmatch/src/test/java/com/sportsmatch/services/UserServiceImpTest.java index c875c297..81f8f0ec 100644 --- a/backend/sportsmatch/src/test/java/com/sportsmatch/services/UserServiceImpTest.java +++ b/backend/sportsmatch/src/test/java/com/sportsmatch/services/UserServiceImpTest.java @@ -106,7 +106,7 @@ void getUserById() { Sport sport = new Sport(); sport.setId(1L); sport.setName("Tennis"); - sport.setEmoji("🎾"); + sport.setEmoji(""); sport.setBackgroundImageURL("./assets/sport-component-tennis.png"); // SportUser @@ -125,17 +125,16 @@ void getUserById() { sports.add(sportDTO); // Event - Event event = mock(Event.class); + Event event = new Event(); event.setSport(sport); - EventPlayer eventPlayer = mock(EventPlayer.class); - - when(eventPlayer.getEvent()).thenReturn(event); - when(eventPlayer.getEvent().getSport()).thenReturn(sport); + // EventPlayer (no need to mock) + EventPlayer eventPlayer = new EventPlayer(); + eventPlayer.setEvent(event); // Set the event directly user.getEventsPlayed().add(eventPlayer); - doNothing().when(rankService).updatePlayersRanks(event); + // No need to mock eventPlayer.getEvent() UserDTO expectedUserDTO = UserDTO.builder().name(user.getName()).elo(user.getRank()).sports(sports).build(); diff --git a/frontend/sportsmatch-app/package-lock.json b/frontend/sportsmatch-app/package-lock.json index 9eb97062..3aaf5a18 100644 --- a/frontend/sportsmatch-app/package-lock.json +++ b/frontend/sportsmatch-app/package-lock.json @@ -8,7 +8,6 @@ "name": "sportsmatch-app", "version": "0.0.0", "dependencies": { - "axios": "^1.6.7", "bootstrap": "^5.3.2", "react": "^18.2.0", "react-bootstrap": "^2.10.0", @@ -2391,7 +2390,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true }, "node_modules/available-typed-arrays": { "version": "1.0.5", @@ -2405,16 +2405,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axios": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz", - "integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==", - "dependencies": { - "follow-redirects": "^1.15.4", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2663,6 +2653,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -2858,6 +2849,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -3854,25 +3846,6 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.5", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.5.tgz", - "integrity": "sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -3886,6 +3859,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -5285,6 +5259,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -5293,6 +5268,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -5896,11 +5872,6 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/psl": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", diff --git a/frontend/sportsmatch-app/src/components/EventHistoryItem.tsx b/frontend/sportsmatch-app/src/components/EventHistoryItem.tsx index 5f008a5e..342c5689 100644 --- a/frontend/sportsmatch-app/src/components/EventHistoryItem.tsx +++ b/frontend/sportsmatch-app/src/components/EventHistoryItem.tsx @@ -1,3 +1,4 @@ +import { useEffect, useState } from 'react' import { EventHistoryDTO } from '../generated/api' import '../styles/EventHistoryItem.css' import Avatar from './Avatar' @@ -7,12 +8,44 @@ interface EventHistoryProps { } function EventHistoryItem({ eventHistoryDTO }: EventHistoryProps) { + const [eventStatus, setEventStatus] = useState('') + + useEffect(() => { + const matchResult = + eventHistoryDTO.userScore! > eventHistoryDTO.opponentScore! + ? 'VICTORY' + : eventHistoryDTO.userScore! == eventHistoryDTO.opponentScore! + ? 'DRAW' + : 'DEFEAT' + if ( + eventHistoryDTO.status?.includes(EventHistoryDTO.status.MATCH) && + !eventHistoryDTO.status?.includes(EventHistoryDTO.status.MISMATCH) + ) { + setEventStatus(matchResult) + } else if ( + eventHistoryDTO.status?.includes( + EventHistoryDTO.status.WAITING_FOR_RATING, + ) + ) { + const result = matchResult + ' (UNCONFIRMED)' + setEventStatus(result) + } else if ( + eventHistoryDTO.status?.includes(EventHistoryDTO.status.MISMATCH) + ) { + setEventStatus('SCORE MISMATCH') + } + }, [ + eventHistoryDTO.opponentScore, + eventHistoryDTO.status, + eventHistoryDTO.userScore, + ]) + return ( <>
-
VICTORY (uncorfirmed)
+
{eventStatus}
@@ -21,7 +54,13 @@ function EventHistoryItem({ eventHistoryDTO }: EventHistoryProps) {
You
- {eventHistoryDTO.userScore} + + {eventHistoryDTO.status?.includes( + EventHistoryDTO.status.MISMATCH, + ) + ? '?' + : eventHistoryDTO.userScore} +
@@ -29,7 +68,13 @@ function EventHistoryItem({ eventHistoryDTO }: EventHistoryProps) {
- {eventHistoryDTO.opponentScore} + + {eventHistoryDTO.status?.includes( + EventHistoryDTO.status.MISMATCH, + ) + ? '?' + : eventHistoryDTO.opponentScore} +
diff --git a/frontend/sportsmatch-app/src/components/JoinEventComponent.tsx b/frontend/sportsmatch-app/src/components/JoinEventComponent.tsx index b657be82..b7b6f2ad 100644 --- a/frontend/sportsmatch-app/src/components/JoinEventComponent.tsx +++ b/frontend/sportsmatch-app/src/components/JoinEventComponent.tsx @@ -49,7 +49,7 @@ export default function JoinEventComponent(p: JoinEventProps) {

You want to join {p.event.sport} {' at ' + - p.event.location + + p.event.placeDTO?.name + ' on ' + date.toLocaleDateString('en', { month: 'long' }) + ' ' + @@ -90,7 +90,7 @@ export default function JoinEventComponent(p: JoinEventProps) {

Your rank is too low/high to join {p.event.sport} at{' '} - {p.event.location} + {p.event.placeDTO?.name}

diff --git a/frontend/sportsmatch-app/src/components/Match.tsx b/frontend/sportsmatch-app/src/components/Match.tsx index 4e55baf2..9a821fe5 100644 --- a/frontend/sportsmatch-app/src/components/Match.tsx +++ b/frontend/sportsmatch-app/src/components/Match.tsx @@ -8,23 +8,42 @@ import { LuSettings2, } from 'react-icons/lu' import { Link } from 'react-router-dom' +import { EventDTO } from '../generated/api/models/EventDTO' +import { useEffect, useState } from 'react' +import { ExSecuredEndpointService, OpenAPI, UserDTO } from '../generated/api' interface InProgressProps { - event: { - id: number - maxElo: number - minElo: number - dateEnd: string - dateStart: string - location: string - title: string - sport: string - playerOne: string - playerTwo?: string - } + event: EventDTO } function InProgress({ event }: InProgressProps) { + const [currentUser, setCurrentUser] = useState() + useEffect(() => { + OpenAPI.TOKEN = localStorage.getItem('token')! + const fetchUserInfo = async () => { + setCurrentUser( + (await ExSecuredEndpointService.getUserMainPage()) as UserDTO, + ) + } + fetchUserInfo() + }, []) + + const eventDate = new Date( + parseInt(event.dateStart[0]), + parseInt(event.dateStart[1]), + parseInt(event.dateStart[2]), + ) + + const eventStartTime = () => { + const hour = event.dateStart[3] + const min = event.dateStart[4] === '0' ? '00' : event.dateStart[4] + return hour + ':' + (min.length === 1 ? min + '0' : min) + } + const eventEndTime = () => { + const hour = event.dateEnd[3] + const min = event.dateEnd[4] === '0' ? '00' : event.dateEnd[4] + return hour + ':' + (min.length === 1 ? min + '0' : min) + } return ( <>
@@ -37,7 +56,7 @@ function InProgress({ event }: InProgressProps) { > - {event.playerTwo === null ? ( + {event.player2Id === null ? (

Matchmaking
in progress @@ -51,13 +70,15 @@ function InProgress({ event }: InProgressProps) {
  • {' '} - {event.playerTwo === null + {event.player2Id === null ? 'Awaiting opponent...' - : event.playerTwo} + : event.player1Name === currentUser?.name + ? event.player2Name + : event.player1Name}
  • - {event.location} + {event.placeDTO?.name}
  • @@ -65,11 +86,23 @@ function InProgress({ event }: InProgressProps) {
  • - {event.dateStart} + {eventDate.getDay() + + '.' + + eventDate.getMonth() + + '.' + + eventDate.getFullYear() + + ', ' + + eventStartTime()}
  • - {event.dateEnd} + {eventDate.getDay() + + '.' + + eventDate.getMonth() + + '.' + + eventDate.getFullYear() + + ', ' + + eventEndTime()}

diff --git a/frontend/sportsmatch-app/src/components/RateGameComponent.tsx b/frontend/sportsmatch-app/src/components/RateGameComponent.tsx index c84a053c..d39dff63 100644 --- a/frontend/sportsmatch-app/src/components/RateGameComponent.tsx +++ b/frontend/sportsmatch-app/src/components/RateGameComponent.tsx @@ -127,7 +127,7 @@ export default function RateGameComponent(p: Props) { {myEvent.dateEnd[3]}:{myEvent.dateEnd[4]}
- {myEvent.location}{' '} + {myEvent.placeDTO?.name}{' '} ) : (
diff --git a/frontend/sportsmatch-app/src/components/SportEvent.test.tsx b/frontend/sportsmatch-app/src/components/SportEvent.test.tsx index 6ee734b9..45987ec5 100644 --- a/frontend/sportsmatch-app/src/components/SportEvent.test.tsx +++ b/frontend/sportsmatch-app/src/components/SportEvent.test.tsx @@ -1,18 +1,24 @@ import { render, screen } from '@testing-library/react' import SportEvent from './SportEvent' +import { EventDTO } from '../generated/api' describe('SportEvent', async () => { - const mockEvent = { + const mockEvent: EventDTO = { id: 1, maxElo: 2000, minElo: 1800, dateEnd: '2024-01-27', dateStart: '2024-01-26', - location: 'Test Location', + placeDTO: { + name: 'Test Location', + address: 'address', + latitude: 51, + longitude: 30, + }, title: 'Test Event', sport: 'Test Sport', - playerOne: 'Player One', - playerTwo: 'Player Two', + player1Name: 'Player One', + player2Name: 'Player Two', } it('renders the component with correct data', () => { render() @@ -34,7 +40,7 @@ describe('SportEvent', async () => { }) it('renders the component without playerTwo when it is not provided', () => { - const { playerTwo, ...mockEventWithoutPlayerTwo } = mockEvent + const { player2Id, ...mockEventWithoutPlayerTwo } = mockEvent render() diff --git a/frontend/sportsmatch-app/src/components/SportEvent.tsx b/frontend/sportsmatch-app/src/components/SportEvent.tsx index 9a638e6f..f7ea2d43 100644 --- a/frontend/sportsmatch-app/src/components/SportEvent.tsx +++ b/frontend/sportsmatch-app/src/components/SportEvent.tsx @@ -26,7 +26,7 @@ function SportEvent({ event }: { event: EventDTO }) {
  • - {event.location} + {event.placeDTO?.name}
  • {event.minElo} - {event.maxElo} diff --git a/frontend/sportsmatch-app/src/generated/api/index.ts b/frontend/sportsmatch-app/src/generated/api/index.ts index e4e996a1..6e0320cf 100644 --- a/frontend/sportsmatch-app/src/generated/api/index.ts +++ b/frontend/sportsmatch-app/src/generated/api/index.ts @@ -10,10 +10,8 @@ export type { OpenAPIConfig } from './core/OpenAPI'; export type { AuthRequestDTO } from './models/AuthRequestDTO'; export type { EventDTO } from './models/EventDTO'; export { EventHistoryDTO } from './models/EventHistoryDTO'; -export type { Pageable } from './models/Pageable'; export type { PlaceDTO } from './models/PlaceDTO'; export type { RatingDTO } from './models/RatingDTO'; -export type { RequestEventDTO } from './models/RequestEventDTO'; export type { SportDTO } from './models/SportDTO'; export type { UserDTO } from './models/UserDTO'; export type { UserInfoDTO } from './models/UserInfoDTO'; diff --git a/frontend/sportsmatch-app/src/generated/api/models/EventDTO.ts b/frontend/sportsmatch-app/src/generated/api/models/EventDTO.ts index 5f61ccd0..9b43b821 100644 --- a/frontend/sportsmatch-app/src/generated/api/models/EventDTO.ts +++ b/frontend/sportsmatch-app/src/generated/api/models/EventDTO.ts @@ -2,11 +2,11 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { PlaceDTO } from './PlaceDTO'; export type EventDTO = { id?: number; dateStart: string; dateEnd: string; - location: string; minElo: number; maxElo: number; title: string; @@ -15,5 +15,6 @@ export type EventDTO = { sport: string; player1Name?: string; player2Name?: string; + placeDTO?: PlaceDTO; }; diff --git a/frontend/sportsmatch-app/src/generated/api/models/Pageable.ts b/frontend/sportsmatch-app/src/generated/api/models/Pageable.ts deleted file mode 100644 index e7e40e89..00000000 --- a/frontend/sportsmatch-app/src/generated/api/models/Pageable.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type Pageable = { - page?: number; - size?: number; - sort?: Array; -}; - diff --git a/frontend/sportsmatch-app/src/generated/api/models/RequestEventDTO.ts b/frontend/sportsmatch-app/src/generated/api/models/RequestEventDTO.ts deleted file mode 100644 index 83f09bce..00000000 --- a/frontend/sportsmatch-app/src/generated/api/models/RequestEventDTO.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* generated using openapi-typescript-codegen -- do no edit */ -/* istanbul ignore file */ -/* tslint:disable */ -/* eslint-disable */ -export type RequestEventDTO = { - sportsName?: Array; - longitude?: number; - latitude?: number; -}; - diff --git a/frontend/sportsmatch-app/src/generated/api/models/SportDTO.ts b/frontend/sportsmatch-app/src/generated/api/models/SportDTO.ts index 6f1929a0..c6658c6b 100644 --- a/frontend/sportsmatch-app/src/generated/api/models/SportDTO.ts +++ b/frontend/sportsmatch-app/src/generated/api/models/SportDTO.ts @@ -6,5 +6,6 @@ export type SportDTO = { name?: string; emoji?: string; backgroundUImageURL?: string; + id?: number; }; diff --git a/frontend/sportsmatch-app/src/generated/api/services/EventsControllerService.ts b/frontend/sportsmatch-app/src/generated/api/services/EventsControllerService.ts index ba66473e..4aa72b60 100644 --- a/frontend/sportsmatch-app/src/generated/api/services/EventsControllerService.ts +++ b/frontend/sportsmatch-app/src/generated/api/services/EventsControllerService.ts @@ -4,8 +4,6 @@ /* eslint-disable */ import type { EventDTO } from '../models/EventDTO'; import type { EventHistoryDTO } from '../models/EventHistoryDTO'; -import type { Pageable } from '../models/Pageable'; -import type { RequestEventDTO } from '../models/RequestEventDTO'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; @@ -73,6 +71,16 @@ export class EventsControllerService { }, }); } + /** + * @returns EventDTO OK + * @throws ApiError + */ + public static getUpcomingMatches(): CancelablePromise> { + return __request(OpenAPI, { + method: 'GET', + url: '/api/v1/event/upcoming-matches', + }); + } /** * @param sportsIds * @returns any OK @@ -90,34 +98,55 @@ export class EventsControllerService { }); } /** - * @param requestEventDto - * @returns any OK + * @param sportsName + * @param longitude + * @param latitude + * @param page Zero-based page index (0..N) + * @param size The size of the page to be returned + * @param sort Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. + * @returns EventDTO OK * @throws ApiError */ public static getNearbyEvents( - requestEventDto: RequestEventDTO, - ): CancelablePromise> { + sportsName?: Array, + longitude?: number, + latitude?: number, + page?: number, + size: number = 20, + sort?: Array, + ): CancelablePromise> { return __request(OpenAPI, { method: 'GET', url: '/api/v1/event/nearby', query: { - 'requestEventDTO': requestEventDto, + 'sportsName': sportsName, + 'longitude': longitude, + 'latitude': latitude, + 'page': page, + 'size': size, + 'sort': sort, }, }); } /** - * @param pageable + * @param page Zero-based page index (0..N) + * @param size The size of the page to be returned + * @param sort Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. * @returns EventHistoryDTO OK * @throws ApiError */ public static getEventsHistory( - pageable: Pageable, + page?: number, + size: number = 20, + sort?: Array, ): CancelablePromise> { return __request(OpenAPI, { method: 'GET', url: '/api/v1/event/event-history', query: { - 'pageable': pageable, + 'page': page, + 'size': size, + 'sort': sort, }, }); } diff --git a/frontend/sportsmatch-app/src/generated/api/services/SportControllerService.ts b/frontend/sportsmatch-app/src/generated/api/services/SportControllerService.ts index ecb9c2fc..8b035721 100644 --- a/frontend/sportsmatch-app/src/generated/api/services/SportControllerService.ts +++ b/frontend/sportsmatch-app/src/generated/api/services/SportControllerService.ts @@ -2,25 +2,30 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -import type { Pageable } from '../models/Pageable'; import type { SportDTO } from '../models/SportDTO'; import type { CancelablePromise } from '../core/CancelablePromise'; import { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class SportControllerService { /** - * @param pageable + * @param page Zero-based page index (0..N) + * @param size The size of the page to be returned + * @param sort Sorting criteria in the format: property,(asc|desc). Default sort order is ascending. Multiple sort criteria are supported. * @returns SportDTO OK * @throws ApiError */ public static getSports( - pageable: Pageable, + page?: number, + size: number = 20, + sort?: Array, ): CancelablePromise> { return __request(OpenAPI, { method: 'GET', url: '/api/v1/sports/all', query: { - 'pageable': pageable, + 'page': page, + 'size': size, + 'sort': sort, }, }); } diff --git a/frontend/sportsmatch-app/src/pages/Home.tsx b/frontend/sportsmatch-app/src/pages/Home.tsx index 5a9a17d0..bcd5206b 100644 --- a/frontend/sportsmatch-app/src/pages/Home.tsx +++ b/frontend/sportsmatch-app/src/pages/Home.tsx @@ -1,31 +1,86 @@ import Match from '../components/Match' -import SportEvent from '../components/SportEvent' import EventHistoryItem from '../components/EventHistoryItem' +import { useEffect, useState } from 'react' +import { + EventDTO, + EventHistoryDTO, + EventsControllerService, + OpenAPI, +} from '../generated/api' +import { Link } from 'react-router-dom' +import '../styles/UserPage.css' function Home() { - const sampleEvent = { - id: 1, - maxElo: 2000, - minElo: 1200, - dateEnd: '2024-05-02', - dateStart: '2024-05-01', - location: 'Prague, Stadium A', - title: 'Badminton match', - sport: 'Badminton', - playerOne: 'johndoe87', - playerTwo: 'jess_ward', - } - const sampleHistoryDTO = { - userScore: 1, - opponentScore: 2, - opponent: { name: 'Opponent' }, - } + const [eventsHistory, setEventsHistory] = useState([]) + const [upcomingMatch, setUpcomingMatch] = useState([]) + + useEffect(() => { + OpenAPI.TOKEN = localStorage.getItem('token')! + const fetchUpcomingMatch = async () => { + setUpcomingMatch(await EventsControllerService.getUpcomingMatches()) + } + fetchUpcomingMatch() + }, []) + + useEffect(() => { + OpenAPI.TOKEN = localStorage.getItem('token')! + const fetchEvents = async () => { + const response = await EventsControllerService.getEventsHistory(0, 3, [ + 'event.dateEnd,desc', + ]) + if (response && response.length > 0) { + setEventsHistory(response) + } + } + fetchEvents() + }, []) return ( <> - - - +
    +
    + {upcomingMatch.length === 0 ?
    : <>} +
    + {upcomingMatch.length === 0 ? ( +
    +
    +

    No upcoming match

    + Find or Host a Match +
    +
    + ) : ( + + )} +
    +
    + {upcomingMatch.length === 0 ?
    : <>} +
    +
    +

    History

    +
    +
    + View all +
    +
    +
    +
    +
    +
    + {upcomingMatch.length === 0 ? ( +

    No match history

    + ) : ( + eventsHistory.map((e, index) => ( + + )) + )} +
    +
    +
    +
    +

    Nearby

    +
    +
    +
    ) } diff --git a/frontend/sportsmatch-app/src/pages/Index.tsx b/frontend/sportsmatch-app/src/pages/Index.tsx index c124236e..0aab59a3 100644 --- a/frontend/sportsmatch-app/src/pages/Index.tsx +++ b/frontend/sportsmatch-app/src/pages/Index.tsx @@ -11,7 +11,6 @@ import { EventsControllerService, ExSecuredEndpointService, OpenAPI, - RequestEventDTO, SportDTO, } from '../generated/api' import useModal from '../hooks/UseModal' @@ -29,6 +28,8 @@ export default function MainPage() { const location = useLocation() const navigate = useNavigate() const { isOpen, toggle } = useModal() + const [page, setPage] = useState(0) + const size = 5 // handle sports name selected from sportButtoncomponent const handleSportSelectionChange = (selectedButtonSports: string[]) => { @@ -52,27 +53,36 @@ export default function MainPage() { useEffect(() => { const fetchData = async () => { - OpenAPI.TOKEN = localStorage.getItem('token')! try { - const requestEventDTO: RequestEventDTO = { - sportsName: selectedSports, - } - const response = - await EventsControllerService.getNearbyEvents(requestEventDTO) - if (!Array.isArray(response) && response.length === 0) { + const response = await EventsControllerService.getNearbyEvents( + selectedSports, + 0, + 0, + page, + size, + ) + if (!Array.isArray(response)) { throw new Error('Failed to fetch event data') } const data: EventDTO[] = response as EventDTO[] // set filtered events based on api response console.log(data) - setFilteredEvent(data) + if (page === 0) { + setFilteredEvent(data as EventDTO[]) + } else { + setFilteredEvent((previousPage) => [ + ...previousPage, + ...(data as EventDTO[]), + ]) + } + //setFilteredEvent(data as EventDTO[]) } catch (error) { console.error(error as ApiError) } } // call the method fetchData() - }, [selectedSports]) + }, [selectedSports, page]) // handle join event pop up after cliking on the event const handleEventSelection = (e: EventDTO) => { @@ -113,6 +123,11 @@ export default function MainPage() { navigate('/app') } + const loadMore = () => { + const nextPage = page + 1 + setPage(nextPage) + } + return ( <>
    @@ -179,6 +194,13 @@ export default function MainPage() {
+ {filteredEvent.length > 0 && ( +
+
+ +
+
+ )}
) diff --git a/frontend/sportsmatch-app/src/styles/EventHistoryItem.css b/frontend/sportsmatch-app/src/styles/EventHistoryItem.css index bd451d3f..6a0f2ad0 100644 --- a/frontend/sportsmatch-app/src/styles/EventHistoryItem.css +++ b/frontend/sportsmatch-app/src/styles/EventHistoryItem.css @@ -26,6 +26,8 @@ box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.05); border: 2px solid var(--sm-secondary-orange); border-radius: 10px; + margin-top: 4px; + margin-bottom: 4px; } .user-side { diff --git a/frontend/sportsmatch-app/src/styles/JoinEvent.css b/frontend/sportsmatch-app/src/styles/JoinEvent.css index d4723337..4102bd7d 100644 --- a/frontend/sportsmatch-app/src/styles/JoinEvent.css +++ b/frontend/sportsmatch-app/src/styles/JoinEvent.css @@ -9,7 +9,10 @@ } -.join-event-wrapper { +.join-event-wrapper { + display: flex; + flex-direction: column; + justify-content: space-around; padding-left: 5%; padding-right: 5%; color: var(--sm-orange); diff --git a/frontend/sportsmatch-app/src/styles/Match.css b/frontend/sportsmatch-app/src/styles/Match.css index b4abb4e8..968d7383 100644 --- a/frontend/sportsmatch-app/src/styles/Match.css +++ b/frontend/sportsmatch-app/src/styles/Match.css @@ -12,7 +12,7 @@ display: flex; flex-direction: column; justify-content: space-between; - padding: 0.563em; + padding: 0.563em; } .match .col ul { @@ -22,6 +22,10 @@ color: #fff; } +.match li{ + color: #fff; +} + .match .col svg { margin-right: 0.5em; } diff --git a/frontend/sportsmatch-app/src/styles/Modal.css b/frontend/sportsmatch-app/src/styles/Modal.css index 2f144a67..48ddd2d7 100644 --- a/frontend/sportsmatch-app/src/styles/Modal.css +++ b/frontend/sportsmatch-app/src/styles/Modal.css @@ -1,6 +1,6 @@ .modal-overlay { z-index: 9999; - position: absolute; + position: fixed; top: 0; left: 0; width: 100vw; @@ -12,7 +12,9 @@ } .modal-box { - display: block; + display: flex; + align-items: center; + justify-content: center; background: transparent; width: 90%; height: 75%; diff --git a/frontend/sportsmatch-app/src/styles/UserPage.css b/frontend/sportsmatch-app/src/styles/UserPage.css index a8056cc3..bd6cd07f 100644 --- a/frontend/sportsmatch-app/src/styles/UserPage.css +++ b/frontend/sportsmatch-app/src/styles/UserPage.css @@ -1,7 +1,3 @@ -.user-page { - margin-top: 64px; -} - .user-page a { font-size: 1em; color: var(--sm-orange);