Работает отображеине и удаление постов с пагинацией

This commit is contained in:
Никита Потапов 2024-06-21 16:02:24 +04:00
parent 8162b23dd2
commit 9e4b5a06a9
13 changed files with 333 additions and 158 deletions

View File

@ -2,9 +2,13 @@ package com.example.nekontakte.core.api;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import com.example.nekontakte.core.configurations.Constants;
import com.example.nekontakte.posts.api.PostDTO;
import com.example.nekontakte.posts.model.PostEntity;
import org.springframework.data.domain.Page;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -13,8 +17,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.nekontakte.posts.service.PostService;
import com.example.nekontakte.users.api.UserDTO;
import com.example.nekontakte.users.api.UserEditDTO;
import com.example.nekontakte.users.service.UserService;
@Controller
@ -23,6 +25,7 @@ public class HomeCotroller {
public static final String URL = "/";
private static final String PAGE_ATTRIBUTE = "page";
private static final String POSTS_ATTRIBUTE = "posts";
private static final String POST_ATTRIBUTE = "post";
private static final String FEED_VIEW = "feed";
private static final String SUBS_VIEW = "subs";
private static final String USER_PROFILE_VIEW = "user-profile";
@ -37,14 +40,18 @@ public class HomeCotroller {
@GetMapping("/feed")
public String getFeed(@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page, Model model) {
List<PostEntity> posts = postsService.getAll();
Page<PostEntity> pageEntities = postsService.getAll(page, 4);
List<PostEntity> posts = pageEntities.getContent();
List<PostDTO> postDTOs = new ArrayList<PostDTO>();
for (PostEntity postEntity : posts) {
PostDTO postDTO = PostDTO.createFromEntity(postEntity);
postDTOs.add(postDTO);
}
model.addAttribute("totalPages", pageEntities.getTotalPages());
model.addAttribute("currentPage", pageEntities.getNumber());
model.addAttribute(PAGE_ATTRIBUTE, page);
model.addAttribute(POSTS_ATTRIBUTE, postDTOs);
model.addAttribute(POST_ATTRIBUTE, new PostDTO());
return FEED_VIEW;
}

View File

@ -1,81 +0,0 @@
package com.example.nekontakte.posts.api;
import com.example.nekontakte.core.api.PageDto;
import com.example.nekontakte.core.api.PageDtoMapper;
import com.example.nekontakte.core.configurations.Constants;
import org.apache.coyote.BadRequestException;
import org.modelmapper.ModelMapper;
import org.springframework.data.domain.PageRequest;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.example.nekontakte.posts.model.PostEntity;
import com.example.nekontakte.posts.service.PostService;
import com.example.nekontakte.users.service.UserService;
import jakarta.validation.Valid;
@RestController
@RequestMapping(Constants.API_URL + "/post")
public class PostController {
private final PostService postService;
private final ModelMapper modelMapper;
public PostController(PostService postService, ModelMapper modelMapper, UserService userService) {
this.modelMapper = modelMapper;
this.postService = postService;
}
private PostEntity toEntity(PostDTO dto) {
return modelMapper.map(dto, PostEntity.class);
}
private PostDTO toDTO(PostEntity entity) {
return modelMapper.map(entity, PostDTO.class);
}
@GetMapping
public PageDto<PostDTO> getAll(@RequestParam(name = "pageNumber", defaultValue = "0") Integer pageNumber,
@RequestParam(name = "pageSize", defaultValue = "5") Integer pageSize) {
PageRequest pageRequest = PageRequest.of(pageNumber, pageSize);
return PageDtoMapper.toDto(postService.getAll(pageRequest), this::toDTO);
}
@GetMapping("/user/{userId}")
public PageDto<PostDTO> getAllByUserId(@PathVariable(name = "userId") Integer userId,
@RequestParam(name = "pageNumber", defaultValue = "0") Integer pageNumber,
@RequestParam(name = "pageSize", defaultValue = "5") Integer pageSize) {
PageRequest pageRequest = PageRequest.of(pageNumber, pageSize);
return PageDtoMapper.toDto(postService.getAllByUserId(userId, pageRequest), this::toDTO);
}
@GetMapping("/{id}")
public PostDTO get(@PathVariable(name = "id") Integer id) {
return toDTO(postService.get(id));
}
@PostMapping
public PostDTO create(@RequestBody @Valid PostDTO PostDTO) throws BadRequestException {
return toDTO(postService.create(toEntity(PostDTO)));
}
@PutMapping("/{id}")
public PostDTO update(@PathVariable(name = "id") Integer id, @RequestBody PostDTO PostDTO) {
return toDTO(postService.update(id, toEntity(PostDTO)));
}
@DeleteMapping("/{id}")
public PostDTO delete(@PathVariable(name = "id") Integer id) {
return toDTO(postService.delete(id));
}
}

View File

@ -1,7 +1,10 @@
package com.example.nekontakte.posts.api;
import java.time.LocalDateTime;
import java.util.Date;
import org.springframework.format.annotation.DateTimeFormat;
import com.example.nekontakte.posts.model.PostEntity;
import com.example.nekontakte.users.api.UserDTO;
import com.fasterxml.jackson.annotation.JsonProperty;
@ -28,11 +31,20 @@ public class PostDTO {
}
@NotNull
private Date pubDate;
@DateTimeFormat(pattern = "dd-MM-yyyy HH:mm")
private LocalDateTime pubDate;
private String image;
private String html;
private String text;
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
public Integer getId() {
return id;
@ -42,11 +54,11 @@ public class PostDTO {
this.id = id;
}
public Date getPubDate() {
public LocalDateTime getPubDate() {
return pubDate;
}
public void setPubDate(Date pubDate) {
public void setPubDate(LocalDateTime pubDate) {
this.pubDate = pubDate;
}
@ -58,14 +70,6 @@ public class PostDTO {
this.image = image;
}
public String getHtml() {
return html;
}
public void setHtml(String html) {
this.html = html;
}
public Integer getUserId() {
return userId;
}
@ -78,7 +82,7 @@ public class PostDTO {
PostDTO post = new PostDTO();
post.setId(entity.getId());
post.setUserId(entity.getUser().getId());
post.setHtml(entity.getHtml());
post.setText(entity.getHtml());
post.setImage(entity.getImage());
post.setPubDate(entity.getPubDate());
post.setUserDTO(UserDTO.createFromEntity(entity.getUser()));

View File

@ -0,0 +1,83 @@
package com.example.nekontakte.posts.api;
import com.example.nekontakte.core.configurations.Constants;
import org.apache.coyote.BadRequestException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import com.example.nekontakte.posts.model.PostEntity;
import com.example.nekontakte.posts.service.PostService;
import com.example.nekontakte.users.service.UserService;
import jakarta.validation.Valid;
@Controller
@RequestMapping(PostsController.URL)
public class PostsController {
public static final String URL = "/posts";
private static final String PAGE_ATTRIBUTE = "page";
private static final String POST_ATTRIBUTE = "post";
private final PostService postService;
private final UserService userService;
public PostsController(PostService postService, UserService userService) {
this.userService = userService;
this.postService = postService;
}
private PostEntity toEntity(PostDTO dto) {
PostEntity entity = new PostEntity();
entity.setId(dto.getId());
entity.setHtml(dto.getText());
entity.setImage(dto.getImage());
entity.setPubDate(dto.getPubDate());
entity.setUser(userService.get(dto.getUserId()));
return entity;
}
private PostDTO toDTO(PostEntity entity) {
return PostDTO.createFromEntity(entity);
}
@PostMapping("/create/{username}")
public String create(Model model,
@PathVariable(name = "username") String username,
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
@ModelAttribute(name = POST_ATTRIBUTE) @Valid PostDTO postDTO,
RedirectAttributes redirectAttributes) throws BadRequestException {
postDTO.setUserId(userService.getByUsername(username).getId());
postService.create(toEntity(postDTO));
redirectAttributes.addAttribute(PAGE_ATTRIBUTE, page);
return Constants.REDIRECT_VIEW + "/feed";
}
@PostMapping("/{id}")
public PostDTO update(@PathVariable(name = "id") Integer id, @RequestBody PostDTO PostDTO) {
return toDTO(postService.update(id, toEntity(PostDTO)));
}
@PostMapping("/delete/{id}")
public String delete(@PathVariable(name = "id") Integer id,
@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page,
RedirectAttributes redirectAttributes,
Model model) {
postService.delete(id);
redirectAttributes.addAttribute(PAGE_ATTRIBUTE, page);
return Constants.REDIRECT_VIEW + "/feed";
}
}

View File

@ -1,5 +1,6 @@
package com.example.nekontakte.posts.model;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Objects;
@ -21,9 +22,9 @@ public class PostEntity extends BaseEntity {
@JoinColumn(name = "userId", nullable = false)
private UserEntity user;
@Temporal(value = TemporalType.DATE)
@Temporal(value = TemporalType.TIMESTAMP)
@Column(nullable = false)
private Date pubDate;
private LocalDateTime pubDate;
@Column
private String image;
@ -42,11 +43,11 @@ public class PostEntity extends BaseEntity {
this.user = user;
}
public Date getPubDate() {
public LocalDateTime getPubDate() {
return pubDate;
}
public void setPubDate(Date pubDate) {
public void setPubDate(LocalDateTime pubDate) {
this.pubDate = pubDate;
}

View File

@ -2,6 +2,7 @@ package com.example.nekontakte.posts.repository;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;

View File

@ -2,13 +2,19 @@ package com.example.nekontakte.posts.service;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Sort.Direction;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Random;
import com.example.nekontakte.core.errors.NotFoundException;
import com.example.nekontakte.posts.model.PostEntity;
import com.example.nekontakte.posts.repository.PostRepository;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.stream.StreamSupport;
@ -20,6 +26,15 @@ public class PostService {
this.repository = repository;
}
public List<PostEntity> getSorted(List<PostEntity> posts) {
posts.sort(new Comparator<PostEntity>() {
public int compare(PostEntity o1, PostEntity o2) {
return o1.getPubDate().compareTo(o2.getPubDate());
}
});
return posts;
}
@Transactional(readOnly = true)
public Page<PostEntity> getAllByUserId(Integer userId, PageRequest pageRequest) {
return repository.findByUserId(userId, pageRequest);
@ -30,6 +45,12 @@ public class PostService {
return repository.findByUserId(userId);
}
@Transactional(readOnly = true)
public Page<PostEntity> getAll(Integer pageNumber, Integer pageSize) {
PageRequest pageRequest = PageRequest.of(pageNumber, pageSize, Sort.by(Direction.DESC, "pubDate"));
return repository.findAll(pageRequest);
}
@Transactional(readOnly = true)
public Page<PostEntity> getAll(PageRequest pageRequest) {
return repository.findAll(pageRequest);
@ -49,6 +70,11 @@ public class PostService {
if (entity.getImage() == null && entity.getHtml() == null) {
throw new org.apache.coyote.BadRequestException("Image or Html must be not null");
}
entity.setPubDate(LocalDateTime.now());
Random rand = new Random();
if (rand.nextBoolean()) {
entity.setImage("https://loremflickr.com/240/320?lock=" + Integer.toString(rand.nextInt(0, 1000000)));
}
return repository.save(entity);
}

View File

@ -68,6 +68,9 @@ public class UserService implements UserDetailsService {
throw new IllegalArgumentException(
String.format("User with username %s is already exists", entity.getUsername()));
}
if (entity.getAvatarImg() == null) {
entity.setAvatarImg("https://i.pravatar.cc/150?u=" + entity.getUsername());
}
return repository.save(entity);
}
@ -78,6 +81,7 @@ public class UserService implements UserDetailsService {
throw new IllegalArgumentException(
String.format("User with username %s is already exists", entity.getUsername()));
}
entity.setAvatarImg("https://i.pravatar.cc/150?u=" + entity.getUsername());
repository.save(entity);
return existsentity;
}

View File

@ -1,3 +1,7 @@
a {
text-decoration: none;
}
html,
body {
height: 100%;
@ -59,3 +63,69 @@ td form {
textarea {
min-height: 1.5em;
}
.post-meta {
font-size: small;
}
.counter-block {
transition: 0.1s;
cursor: pointer;
}
.counter-block:hover {
background-color: #e8e8e8;
}
.post-body-img {
width: 100%;
}
.post-body a {
color: #4a9cb7;
text-decoration: none;
background-image: linear-gradient(currentColor, currentColor);
background-position: 0% 100%;
background-repeat: no-repeat;
background-size: 0% 2px;
transition: background-size 0.3s;
}
.post-body a:hover {
background-size: 100% 2px;
}
.post-body-text {
white-space: pre-wrap;
}
.avatar-small {
width: 35px;
height: 35px;
object-fit: cover;
}
.username-link {
transition: 0.3s;
display: flex;
}
.username-link::before {
content: "@";
display: flex;
flex-direction: row;
padding: 0;
margin: 0;
}
.username-link::hover {
text-decoration: underline;
}
.b1 {
border: none;
background: none;
cursor: pointer;
margin: 0;
padding: 0;
}

View File

@ -12,17 +12,21 @@
<body>
<main layout:fragment="content">
<div class="container d-flex flex-column w-50 flex-fill">
<div
class="container d-flex flex-column w-50 flex-fill"
sec:authorize="isAuthenticated()"
>
<form
sec:authorize="isAuthenticated()"
method="post"
th:action="@{/posts/create/{username}(username=${#authentication.name})}"
class="d-flex flex-column mb-3 mt-2"
th:object="${post}"
>
<!-- <input type="hidden" th:name="page" th:value="${page}" /> -->
<textarea
name="postBody"
class="mb-3"
placeholder="Напишите что-нибудь..."
th:field="*{text}"
required
class="mb-3 form-control"
></textarea>
<button type="submit" class="btn btn-primary">Опубликовать</button>
</form>
@ -39,9 +43,9 @@
</th:block>
<th:block
th:replace="~{ pagination :: pagination (
url=${'admin/users'},
totalPages=${totalPages},
currentPage=${currentPage}) }"
url=${'feed'},
totalPages=${totalPages},
currentPage=${currentPage}) }"
/>
</th:block>
</div>

View File

@ -80,8 +80,7 @@
</a>
<a
class="nav-link"
th:href="@{/me}"
th:classappend="${activeLink.startsWith('/me') ? 'active' : ''}"
th:href="@{/user/{username}(username=${#authentication.name})}"
>
Профиль
</a>

View File

@ -1,48 +1,105 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div th:fragment="post (post)" class="post mb-2 mb-sm-4 w-100 d-flex flex-column border rounded-2" id="post-${post.id}">
<div class="post-header py-1 ps-1 pe-2 d-flex border-bottom justify-content-between align-items-center">
<Link to="@{/user/${post.user.username}}"
class="hoverable d-flex justify-content-start align-items-center rounded-pill px-1">
<div class="post-author-avatar-wrapper">
<img src="#" class="post-author-avatar avatar-small rounded-circle" />
</div>
<div class="post-meta mx-2 d-flex flex-column justify-content-center">
<div class="post-author-name">
[[${post.user.name}]] [[${post.user.surname}]]
</div>
<div class="post-publication-datetime">
[[${post.pubDate}]]
</div>
</div>
</Link>
<div class='d-flex'>
<i class='fs-6 bi bi-pencil' title='Редактировать пост'></i>
<i class='fs-6 bi bi-trash' title='Удалить пост'></i>
</div>
</div>
<div class="post-body">
<div th:if="${post.html != null}" class="post-body-text m-2">
[[${post.html}]]
</div>
<body>
<div
th:fragment="post (post)"
class="post mb-2 mb-sm-4 w-100 d-flex flex-column border rounded-2"
id="post-${post.id}"
>
<div
class="post-header py-1 ps-1 pe-2 d-flex border-bottom justify-content-between align-items-center"
>
<div
class="hoverable d-flex justify-content-start align-items-center rounded-pill px-1"
>
<div class="post-author-avatar-wrapper">
<img
th:src="${post.userDTO.avatarImg}"
class="post-author-avatar avatar-small rounded-circle"
/>
</div>
<div class="post-meta mx-2 d-flex flex-column justify-content-center">
<div class="post-author-name d-flex">
<div
class="d-flex me-1"
th:unless="${#strings.isEmpty(post.userDTO.name) || #strings.isEmpty(post.userDTO.name)}"
>
[[${post.userDTO.name}]] [[${post.userDTO.surname}]]
</div>
<a
th:href="@{/user/{username}(username=${post.userDTO.username})}"
class="username-link"
>
[[${post.userDTO.username}]]
</a>
</div>
<div
class="post-publication-datetime"
th:text="${#temporals.format(post.pubDate, 'yyyy-MM-dd HH:mm')}"
></div>
</div>
</div>
<div sec:authorize="${hasRole('ADMIN')}" class="d-flex">
<th:block th:replace="~{::post-actions (${post.id})}"> </th:block>
</div>
<div
sec:authorize="${hasRole('USER')}"
th:if="${#authentication.name == post.userDTO.username}"
class="d-flex"
>
<th:block th:replace="~{::post-actions (${post.id})}"> </th:block>
</div>
</div>
<div class="post-body">
<div
th:unless="${#strings.isEmpty(post.text)}"
class="post-body-text m-2"
th:text="${post.text}"
></div>
<img th:if="${post.image != null}" src="${post.image}" class="post-body-img img-fluid" />
</div>
<div class="post-footer py-1 px-2 border-top d-flex">
<div class="hoverable counter-block likes-block px-2 me-1 d-flex align-items-center rounded-4">
<svg height="24" viewBox="0 0 24 24" width="24" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fillRule="evenodd">
<path d="M0 0h24v24H0z"></path>
<path
d="M16 4a5.95 5.95 0 0 0-3.89 1.7l-.12.11-.12-.11A5.96 5.96 0 0 0 7.73 4 5.73 5.73 0 0 0 2 9.72c0 3.08 1.13 4.55 6.18 8.54l2.69 2.1c.66.52 1.6.52 2.26 0l2.36-1.84.94-.74c4.53-3.64 5.57-5.1 5.57-8.06A5.73 5.73 0 0 0 16.27 4zm.27 1.8a3.93 3.93 0 0 1 3.93 3.92v.3c-.08 2.15-1.07 3.33-5.51 6.84l-2.67 2.08a.04.04 0 0 1-.04 0L9.6 17.1l-.87-.7C4.6 13.1 3.8 11.98 3.8 9.73A3.93 3.93 0 0 1 7.73 5.8c1.34 0 2.51.62 3.57 1.92a.9.9 0 0 0 1.4-.01c1.04-1.3 2.2-1.91 3.57-1.91z"
fill="currentColor" fillRule="nonzero"></path>
</g>
</svg>
<span class="count ms-1">734</span>
</div>
</div>
</div>
</body>
<img
th:if="${post.image != null}"
th:src="${post.image}"
class="post-body-img img-fluid"
/>
</div>
<div class="post-footer py-1 px-2 border-top d-flex">
<div
class="hoverable counter-block likes-block px-2 me-1 d-flex align-items-center rounded-4"
>
<svg
height="24"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<g fill="none" fillRule="evenodd">
<path d="M0 0h24v24H0z"></path>
<path
d="M16 4a5.95 5.95 0 0 0-3.89 1.7l-.12.11-.12-.11A5.96 5.96 0 0 0 7.73 4 5.73 5.73 0 0 0 2 9.72c0 3.08 1.13 4.55 6.18 8.54l2.69 2.1c.66.52 1.6.52 2.26 0l2.36-1.84.94-.74c4.53-3.64 5.57-5.1 5.57-8.06A5.73 5.73 0 0 0 16.27 4zm.27 1.8a3.93 3.93 0 0 1 3.93 3.92v.3c-.08 2.15-1.07 3.33-5.51 6.84l-2.67 2.08a.04.04 0 0 1-.04 0L9.6 17.1l-.87-.7C4.6 13.1 3.8 11.98 3.8 9.73A3.93 3.93 0 0 1 7.73 5.8c1.34 0 2.51.62 3.57 1.92a.9.9 0 0 0 1.4-.01c1.04-1.3 2.2-1.91 3.57-1.91z"
fill="currentColor"
fillRule="nonzero"
></path>
</g>
</svg>
<span class="count ms-1">734</span>
</div>
</div>
</div>
<th:block th:fragment="post-actions (postId)">
<i
class="fs-6 bi bi-pencil me-2 text-warning-emphasis"
title="Редактировать пост"
></i>
<form th:action="@{/posts/delete/{id}(id=${postId})}" method="post">
<input type="hidden" th:name="page" th:value="${page}" />
<button type="submit" class="b1">
<i
class="fs-6 bi bi-trash text-danger-emphasis"
title="Удалить пост"
></i>
</button>
</form>
</th:block>
</body>
</html>