Skip to content

Commit

Permalink
Event Page (#8046)
Browse files Browse the repository at this point in the history
* event page

* formatter

* checkstyle

* sonar issues fix

* small fix

* issue fix

* small fixes

* ref code so that instead of .skip.limit we use service and repo methods.

* move model attributes names + data time formatter to constants

* move error messages to constants + add check for null dates

* formatter + checkstyle

* small fixes after code rabbit review
  • Loading branch information
holotsvan authored Jan 29, 2025
1 parent 4235c32 commit aeebe5a
Show file tree
Hide file tree
Showing 23 changed files with 1,051 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@
import greencity.client.RestClient;
import greencity.constant.HttpStatuses;
import greencity.dto.PageableAdvancedDto;
import greencity.dto.event.AbstractEventDateLocationDto;
import greencity.dto.event.AddEventDtoRequest;
import greencity.dto.event.EventAttenderDto;
import greencity.dto.event.EventDateLocationDto;
import greencity.dto.event.EventDto;
import greencity.dto.event.UpdateEventRequestDto;
import greencity.dto.filter.FilterEventDto;
import greencity.dto.user.UserProfilePictureDto;
import greencity.enums.TagType;
import greencity.service.EventService;
import greencity.service.TagsService;
Expand All @@ -23,6 +27,8 @@
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.http.HttpStatus;
Expand All @@ -42,16 +48,41 @@
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import static greencity.constant.ErrorMessage.DATES_COULD_NOT_BE_NULL;
import static greencity.constant.ErrorMessage.DATES_LIST_COULD_NOT_BE_EMPTY;
import static greencity.constant.ManagementConstant.ADD_EVENT_DTO_REQUEST;
import static greencity.constant.ManagementConstant.ATTENDERS_PAGE;
import static greencity.constant.ManagementConstant.AUTHOR;
import static greencity.constant.ManagementConstant.BACKEND_ADDRESS_ATTRIBUTE;
import static greencity.constant.ManagementConstant.CITIES;
import static greencity.constant.ManagementConstant.DATE_TIME_FORMATTER;
import static greencity.constant.ManagementConstant.EVENT_ATTENDERS;
import static greencity.constant.ManagementConstant.EVENT_ATTENDERS_AVATARS;
import static greencity.constant.ManagementConstant.EVENT_DTO;
import static greencity.constant.ManagementConstant.EVENT_TAGS;
import static greencity.constant.ManagementConstant.FILTER_EVENT_DTO;
import static greencity.constant.ManagementConstant.FORMATTED_DATE;
import static greencity.constant.ManagementConstant.GOOGLE_MAP_API_KEY;
import static greencity.constant.ManagementConstant.IMAGES;
import static greencity.constant.ManagementConstant.IMAGE_URLS;
import static greencity.constant.ManagementConstant.PAGEABLE;
import static greencity.constant.ManagementConstant.PAGE_SIZE;
import static greencity.constant.ManagementConstant.SORT_MODEL;
import static greencity.constant.ManagementConstant.USERS_DISLIKED_PAGE;
import static greencity.constant.ManagementConstant.USERS_LIKED_PAGE;
import static greencity.constant.SwaggerExampleModel.UPDATE_EVENT;

@Slf4j
@Controller
@RequiredArgsConstructor
@RequestMapping("/management/events")
public class ManagementEventController {
public static final String BACKEND_ADDRESS_ATTRIBUTE = "backendAddress";
private final EventService eventService;
private final TagsService tagsService;
private final RestClient restClient;
Expand Down Expand Up @@ -83,7 +114,7 @@ public String getAllEvents(@RequestParam(required = false, name = "query") Strin
} else {
allEvents = eventService.getEventsManagement(pageable, filterEventDto, null);
}
model.addAttribute("pageable", allEvents);
model.addAttribute(PAGEABLE, allEvents);
Sort sort = pageable.getSort();
StringBuilder orderUrl = new StringBuilder();
if (!sort.isEmpty()) {
Expand All @@ -94,12 +125,12 @@ public String getAllEvents(@RequestParam(required = false, name = "query") Strin
orderUrl.append("sort=").append(order.getProperty()).append(",").append(order.getDirection().name());
}
}
model.addAttribute("filterEventDto", filterEventDto);
model.addAttribute("sortModel", orderUrl.toString());
model.addAttribute("eventsTag", tagsService.findByTypeAndLanguageCode(TagType.EVENT, locale.getLanguage()));
model.addAttribute("pageSize", pageable.getPageSize());
model.addAttribute(FILTER_EVENT_DTO, filterEventDto);
model.addAttribute(SORT_MODEL, orderUrl.toString());
model.addAttribute(EVENT_TAGS, tagsService.findByTypeAndLanguageCode(TagType.EVENT, locale.getLanguage()));
model.addAttribute(PAGE_SIZE, pageable.getPageSize());
model.addAttribute(BACKEND_ADDRESS_ATTRIBUTE, backendAddress);
model.addAttribute("cities",
model.addAttribute(CITIES,
eventService.getAllEventsAddresses().stream()
.map(e -> "en".equals(locale.getLanguage()) ? e.getCityEn() : e.getCityUa())
.distinct()
Expand All @@ -108,13 +139,63 @@ public String getAllEvents(@RequestParam(required = false, name = "query") Strin
return "core/management_events";
}

@GetMapping("/{eventId}")
public String getEvent(Model model, @PathVariable("eventId") Long eventId,
@Parameter(hidden = true) Principal principal) {
EventDto eventDto = eventService.getEvent(eventId, principal);
Set<EventAttenderDto> eventAttenders = eventService.getAllEventAttenders(eventId);
List<String> attendersAvatars = eventAttenders.stream()
.map(EventAttenderDto::getImagePath)
.filter(Objects::nonNull)
.toList();
model.addAttribute(EVENT_DTO, eventDto);
model.addAttribute(FORMATTED_DATE, getFormattedDates(eventDto.getDates()));
model.addAttribute(IMAGE_URLS, getImageUrls(eventDto));
model.addAttribute(EVENT_ATTENDERS, eventAttenders);
model.addAttribute(EVENT_ATTENDERS_AVATARS, attendersAvatars);
return "core/management_event";
}

@GetMapping("/{eventId}/attenders")
public String getAttenders(@PathVariable("eventId") Long eventId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Model model) {
Pageable pageable = PageRequest.of(page, size);
Page<EventAttenderDto> attandersPage = eventService.getAttendersPage(eventId, pageable);
model.addAttribute(ATTENDERS_PAGE, attandersPage);
return "core/fragments/attenders-table";
}

@GetMapping("/{eventId}/likes")
public String getUsersLikedEvent(@PathVariable("eventId") Long eventId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Model model) {
Pageable pageable = PageRequest.of(page, size);
Page<UserProfilePictureDto> usersLikedPage = eventService.getUsersLikedEventPage(eventId, pageable);
model.addAttribute(USERS_LIKED_PAGE, usersLikedPage);
return "core/fragments/likes-table";
}

@GetMapping("/{eventId}/dislikes")
public String getUsersDislikedEvent(@PathVariable("eventId") Long eventId,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "10") int size,
Model model) {
Pageable pageable = PageRequest.of(page, size);
Page<UserProfilePictureDto> usersDislikedPage = eventService.getUsersDislikedEventPage(eventId, pageable);
model.addAttribute(USERS_DISLIKED_PAGE, usersDislikedPage);
return "core/fragments/dislikes-table";
}

@GetMapping("/create-event")
public String getEventCreatePage(Model model, Principal principal) {
model.addAttribute("addEventDtoRequest", new AddEventDtoRequest());
model.addAttribute("images", new MultipartFile[] {});
model.addAttribute(ADD_EVENT_DTO_REQUEST, new AddEventDtoRequest());
model.addAttribute(IMAGES, new MultipartFile[] {});
model.addAttribute(BACKEND_ADDRESS_ATTRIBUTE, backendAddress);
model.addAttribute("author", restClient.findByEmail(principal.getName()).getName());
model.addAttribute("googleMapApiKey", googleMapApiKey);
model.addAttribute(AUTHOR, restClient.findByEmail(principal.getName()).getName());
model.addAttribute(GOOGLE_MAP_API_KEY, googleMapApiKey);
return "core/management_create_event";
}

Expand All @@ -123,8 +204,8 @@ public String createEvent(@RequestPart("addEventDtoRequest") AddEventDtoRequest
@RequestPart("images") MultipartFile[] images,
Principal principal,
Model model) {
model.addAttribute("addEventDtoRequest", new AddEventDtoRequest());
model.addAttribute("images", new MultipartFile[] {});
model.addAttribute(ADD_EVENT_DTO_REQUEST, new AddEventDtoRequest());
model.addAttribute(IMAGES, new MultipartFile[] {});
eventService.save(addEventDtoRequest, principal.getName(), images);
return "redirect:/management/events";
}
Expand Down Expand Up @@ -165,9 +246,37 @@ public ResponseEntity<EventDto> update(
@GetMapping("/edit/{id}")
public String editEvent(@PathVariable("id") Long id, Model model, Principal principal) {
model.addAttribute(BACKEND_ADDRESS_ATTRIBUTE, backendAddress);
model.addAttribute("author", restClient.findByEmail(principal.getName()).getName());
model.addAttribute("eventDto", eventService.getEvent(id, principal));
model.addAttribute("googleMapApiKey", googleMapApiKey);
model.addAttribute(AUTHOR, restClient.findByEmail(principal.getName()).getName());
model.addAttribute(EVENT_DTO, eventService.getEvent(id, principal));
model.addAttribute(GOOGLE_MAP_API_KEY, googleMapApiKey);
return "core/management_edit_event";
}

private String getFormattedDates(List<EventDateLocationDto> dates) {
if (Objects.isNull(dates)) {
throw new IllegalArgumentException(DATES_COULD_NOT_BE_NULL);
}

EventDateLocationDto firstDateDto = dates.stream()
.min(Comparator.comparing(AbstractEventDateLocationDto::getStartDate))
.orElseThrow(() -> new IllegalArgumentException(DATES_LIST_COULD_NOT_BE_EMPTY));

EventDateLocationDto lastDateDto = dates.stream()
.max(Comparator.comparing(AbstractEventDateLocationDto::getFinishDate))
.orElseThrow(() -> new IllegalArgumentException(DATES_LIST_COULD_NOT_BE_EMPTY));

if (firstDateDto.getStartDate().toLocalDate().equals(lastDateDto.getStartDate().toLocalDate())) {
return firstDateDto.getStartDate().format(DATE_TIME_FORMATTER);
} else {
return firstDateDto.getStartDate().format(DATE_TIME_FORMATTER) + " - "
+ lastDateDto.getFinishDate().format(DATE_TIME_FORMATTER);
}
}

private List<String> getImageUrls(EventDto eventDto) {
List<String> urls = new ArrayList<>();
urls.add(eventDto.getTitleImage());
urls.addAll(Objects.nonNull(eventDto.getAdditionalImages()) ? eventDto.getAdditionalImages() : List.of());
return urls;
}
}
17 changes: 17 additions & 0 deletions core/src/main/resources/static/css/avatars.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.avatar-container {
display: inline-flex;
}

.avatar-wrapper {
position: relative;
width: 30px;
height: 30px;
margin-right: -10px;
}

.avatar {
width: 100%;
height: 100%;
border: 2px solid var(--white);
border-radius: 50%;
}
24 changes: 24 additions & 0 deletions core/src/main/resources/static/css/carousel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.carousel-item img {
max-height: 400px;
object-fit: contain;
margin: 0 auto;
}

.carousel-indicators li {
background-color: lightgray;
border-radius: 50%;
width: 10px;
height: 10px;
aspect-ratio: 1/1 !important;
border:0;
margin: 0 5px;
}

.carousel-indicators .active {
background-color: var(--green);
}

.carousel-inner {
width: 100%;
height: 400px;
}
32 changes: 32 additions & 0 deletions core/src/main/resources/static/css/dropdown.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/* dropdown.css */
.dropdown__header {
display: flex;
align-items: center;
cursor: pointer;
}

.dropdown__title {
margin: 0;
font-size: 2rem;
font-weight: 500;
text-decoration: underline 1px solid var(--black-ash);
color: var(--black-ash);
}

.dropdown__arrow {
margin-left: 8px;
font-size: 1.25rem;
color: var(--black-ash);

}

.dropdown__content {
position: absolute;
top: 100%;
left: 0;
width: 100%;
margin-top: 5px;
padding: 15px;
box-shadow: var(--black-shadow);
display: none;
}
21 changes: 21 additions & 0 deletions core/src/main/resources/static/management/events/carousel.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
function initializeCarousel(carouselId, imageUrls) {
let numImages = imageUrls ? imageUrls.length : 0;
let indicatorsHtml = "";
let innerHtml = "";
if (numImages > 0) {
for (let i = 0; i < numImages; i++) {
indicatorsHtml += `
<li data-target="#${carouselId}" data-slide-to="${i}"${i === 0 ? ' class="active"' : ''}></li>
`;
innerHtml += `
<div class="carousel-item${i === 0 ? ' active' : ''}">
<img src="${imageUrls[i]}" class="d-block w-100" alt="Image ${i + 1}">
</div>
`;
}
$('#' + carouselId + ' .carousel-indicators').html(indicatorsHtml);
$('#' + carouselId + ' .carousel-inner').html(innerHtml);
} else {
$('#' + carouselId).hide();
}
}
8 changes: 8 additions & 0 deletions core/src/main/resources/static/management/events/dropdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"use strict";

function toggleDropdown(headerElement) {
const dropdownContent = headerElement.nextElementSibling;
const arrow = headerElement.querySelector(".dropdown__arrow");
dropdownContent.style.display = dropdownContent.style.display === "none" ? "block" : "none";
arrow.textContent = dropdownContent.style.display === "block" ? "▲" : "▼";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div id="attendersTable" style="display:block" th:fragment="attendersTable">
<div class="table-responsive mb-4 mt-4">
<div class="table-wrapper">
<table class="table table-bordered">
<thead>
<tr>
<th>ID</th>
<th>Username</th>
<th>Avatar</th>
</tr>
</thead>
<tbody>
<tr th:each="attender : ${attendersPage.content}">
<td class="text-align-center">
<a th:href="@{/management/users/{id}(id=${attender.getId()})}" th:text="${attender.getId()}"></a>
</td>
<td th:text="${attender.name}"></td>
<td>
<img th:if="attender.imagePath ne null or attender.imagePath ne ''" th:src="${attender.imagePath}" th:alt="'Avatar for user with id ' + ${attender.id}">
<p th:if="attender.imagePath == null or attender.imagePath == ''">No Avatar</p>
</td>
</tr>
</tbody>
</table>
</div>

<!-- Pagination -->
<div class="clearfix" th:if="${attendersPage.content.size() > 0}">
<ul class="pagination">
<li class="page-item" th:classappend="${attendersPage.isFirst()} ? 'disabled'">
<a class="page-link" th:onclick="'loadDropdownContent(\'attenders\',' + ${attendersPage.number - 1} + ')'">Previous</a>
</li>
<li class="page-item" th:each="pageNumber : ${#numbers.sequence(0, attendersPage.totalPages - 1)}"
th:classappend="${pageNumber == attendersPage.number} ? 'active'">
<a class="page-link" th:onclick="'loadDropdownContent(\'attenders\',' + ${pageNumber} + ')'"
th:text="${pageNumber + 1}"></a>
</li>
<li class="page-item" th:classappend="${attendersPage.isLast()} ? 'disabled'">
<a class="page-link" th:onclick="'loadDropdownContent(\'attenders\',' + ${attendersPage.number + 1} + ')'">Next</a>
</li>
</ul>
</div>
</div>
</div>
</body>
</html>
15 changes: 15 additions & 0 deletions core/src/main/resources/templates/core/fragments/avatars.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<body>
<span th:fragment="avatarList(images, maxImages)">
<div th:if="${images != null and not #lists.isEmpty(images)}" class="avatar-container">
<div th:each="image, iterStat : ${images}"
th:if="${iterStat.index < maxImages}"
class="avatar-wrapper"
th:style="|z-index: ${iterStat.index + 1}|">
<img th:src="@{${image}}" class="rounded-circle avatar">
</div>
</div>
</span>
</body>
</html>
Loading

0 comments on commit aeebe5a

Please sign in to comment.