Compare commits

...

2 Commits

11 changed files with 436 additions and 176 deletions

View File

@ -1,7 +1,6 @@
package com.example.nekontakte.core.api;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import jakarta.servlet.http.HttpServletRequest;

View File

@ -1,33 +1,50 @@
package com.example.nekontakte.core.api;
import org.springframework.boot.context.properties.bind.Name;
import java.util.ArrayList;
import java.util.List;
import com.example.nekontakte.posts.api.PostDTO;
import com.example.nekontakte.posts.model.PostEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.nekontakte.posts.service.PostService;
import jakarta.websocket.server.PathParam;
import com.example.nekontakte.users.api.UserDTO;
import com.example.nekontakte.users.api.UserEditDTO;
import com.example.nekontakte.users.service.UserService;
@Controller
@RequestMapping(HomeCotroller.URL)
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 FEED_VIEW = "feed";
private static final String SUBS_VIEW = "subs";
private static final String USER_PROFILE_VIEW = "user-profile";
private final PostService postsService;
private final UserService userService;
public HomeCotroller(PostService postsService) {
public HomeCotroller(PostService postsService, UserService userService) {
this.postsService = postsService;
this.userService = userService;
}
@GetMapping("/feed")
public String getFeed(Model model) {
public String getFeed(@RequestParam(name = PAGE_ATTRIBUTE, defaultValue = "0") int page, Model model) {
List<PostEntity> posts = postsService.getAll();
List<PostDTO> postDTOs = new ArrayList<PostDTO>();
for (PostEntity postEntity : posts) {
PostDTO postDTO = PostDTO.createFromEntity(postEntity);
postDTOs.add(postDTO);
}
model.addAttribute(PAGE_ATTRIBUTE, page);
model.addAttribute(POSTS_ATTRIBUTE, postDTOs);
return FEED_VIEW;
}

View File

@ -2,6 +2,8 @@ package com.example.nekontakte.posts.api;
import java.util.Date;
import com.example.nekontakte.posts.model.PostEntity;
import com.example.nekontakte.users.api.UserDTO;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotNull;
@ -14,6 +16,17 @@ public class PostDTO {
@NotNull
private Integer userId;
private UserDTO userDTO;
public UserDTO getUserDTO() {
return userDTO;
}
public void setUserDTO(UserDTO userDTO) {
this.userDTO = userDTO;
this.userId = userDTO.getId();
}
@NotNull
private Date pubDate;
@ -61,4 +74,14 @@ public class PostDTO {
this.userId = userId;
}
public static PostDTO createFromEntity(PostEntity entity) {
PostDTO post = new PostDTO();
post.setId(entity.getId());
post.setUserId(entity.getUser().getId());
post.setHtml(entity.getHtml());
post.setImage(entity.getImage());
post.setPubDate(entity.getPubDate());
post.setUserDTO(UserDTO.createFromEntity(entity.getUser()));
return post;
}
}

View File

@ -55,6 +55,7 @@ public class AdminUserController {
@GetMapping("/create")
public String getCreateView(Model model) {
model.addAttribute(USER_ATTRIBUTE, new UserEditDTO());
model.addAttribute("title", "Новый пользователь");
return USER_EDIT_VIEW;
}
@ -67,6 +68,8 @@ public class AdminUserController {
throw new IllegalArgumentException();
}
model.addAttribute("title", "Редактировать пользователя");
UserEntity userEntity = userService.get(id);
UserEditDTO userEditDTO = new UserEditDTO();
userEditDTO.setId(userEntity.getId());
@ -91,6 +94,8 @@ public class AdminUserController {
Model model,
RedirectAttributes redirectAttributes) {
model.addAttribute("title", "Редактировать пользователя");
UserEntity userEntity;
if (userEditDTO.getId() == null) {

View File

@ -1,7 +1,8 @@
package com.example.nekontakte.users.api;
import java.sql.Date;
import java.util.Date;
import com.example.nekontakte.users.model.UserEntity;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
@ -36,6 +37,19 @@ public class UserDTO {
private String status;
public static UserDTO createFromEntity(UserEntity userEntity) {
UserDTO dto = new UserDTO();
dto.setUsername(userEntity.getUsername());
dto.setPassword(userEntity.getPassword());
dto.setName(userEntity.getName());
dto.setSurname(userEntity.getSurname());
dto.setCity(userEntity.getCity());
dto.setBirthday(userEntity.getBirthday());
dto.setAvatarImg(userEntity.getAvatarImg());
dto.setStatus(userEntity.getStatus());
return dto;
}
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
public Integer getId() {
return id;

View File

@ -1,67 +1,61 @@
html,
body {
height: 100%;
height: 100%;
}
h1 {
font-size: 1.5em;
font-size: 1.5em;
}
h2 {
font-size: 1.25em;
font-size: 1.25em;
}
h3 {
font-size: 1.1em;
font-size: 1.1em;
}
td form {
margin: 0;
padding: 0;
margin-top: -.25em;
margin: 0;
padding: 0;
margin-top: -0.25em;
}
.button-fixed-width {
width: 150px;
width: 150px;
}
.button-link {
padding: 0;
padding: 0;
}
.invalid-feedback {
display: block;
display: block;
}
.w-10 {
width: 10% !important;
width: 10% !important;
}
.my-navbar {
background-color: #3c3c3c !important;
background-color: #3c3c3c !important;
}
.my-navbar .link a:hover {
text-decoration: underline;
text-decoration: underline;
}
.my-navbar .logo {
width: 26px;
height: 26px;
width: 26px;
height: 26px;
}
.my-footer {
background-color: #2c2c2c;
height: 32px;
color: rgba(255, 255, 255, 0.5);
background-color: #2c2c2c;
height: 32px;
color: rgba(255, 255, 255, 0.5);
}
.cart-image {
width: 3.1rem;
padding: 0.25rem;
border-radius: 0.5rem;
textarea {
min-height: 1.5em;
}
.cart-item {
height: auto;
}

View File

@ -1,63 +1,132 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{index}">
<html
lang="ru"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{index}"
>
<head>
<title>[[${title}]]</title>
</head>
<head>
<title>Редакторовать пользователя</title>
</head>
<body>
<main layout:fragment="content">
<form action="#" th:action="@{/admin/users/edit(page=${page})}" th:object="${user}" method="post">
<input type="hidden" th:field="*{id}" id="id" class="form-control" readonly>
<div class="mb-3">
<label for="username" class="form-label">Логин*</label>
<input type="text" th:field="*{username}" id="username" class="form-control">
<div th:if="${#fields.hasErrors('username')}" th:errors="*{username}" class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="userRole" class="form-label">Роль в системе*</label>
<select th:field="*{role}" id="userRole" class="form-control">
<option
th:each="userRoleOpt : ${T(com.example.nekontakte.users.model.UserRole).values()}"
th:value="${userRoleOpt}"
th:text="${userRoleOpt.displayName}">
</option>
</select>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" th:field="*{password}" id="password" class="form-control">
<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}" class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="passwordConfirm" class="form-label">Пароль (подтверждение)</label>
<input type="password" th:field="*{passwordConfirm}" id="passwordConfirm" class="form-control">
<div th:if="${#fields.hasErrors('passwordConfirm')}" th:errors="*{passwordConfirm}" class="invalid-feedback"></div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Имя</label>
<input type="text" th:field="*{name}" id="name" class="form-control">
</div>
<div class="mb-3">
<label for="surname" class="form-label">Имя пользователя</label>
<input type="text" th:field="*{surname}" id="surname" class="form-control">
</div>
<div class="mb-3">
<label for="birthday" class="form-label">Дата рождения</label>
<input type="date" th:field="*{birthday}" id="birthday" class="form-control">
</div>
<div class="mb-3">
<label for="city" class="form-label">Место проживания</label>
<input type="text" th:field="*{city}" id="city" class="form-control">
</div>
<div class="mb-3">
<label for="status" class="form-label">Статус</label>
<input type="text" th:field="*{status}" id="status" class="form-control">
</div>
<button class="btn btn-success me-3" type="submit">Применить</button>
<a th:href="@{/admin/users(page=${page})}" class="btn btn-danger" >Отмена</a>
</form>
</main>
</body>
</html>
<body>
<main layout:fragment="content">
<form
action="#"
th:action="@{/admin/users/edit(page=${page})}"
th:object="${user}"
method="post"
>
<input
type="hidden"
th:field="*{id}"
id="id"
class="form-control"
readonly
/>
<div class="mb-3">
<label for="username" class="form-label">Логин*</label>
<input
type="text"
th:field="*{username}"
id="username"
class="form-control"
/>
<div
th:if="${#fields.hasErrors('username')}"
th:errors="*{username}"
class="invalid-feedback"
></div>
</div>
<div class="mb-3">
<label for="userRole" class="form-label">Роль в системе*</label>
<select th:field="*{role}" id="userRole" class="form-control">
<option
th:each="userRoleOpt : ${T(com.example.nekontakte.users.model.UserRole).values()}"
th:value="${userRoleOpt}"
th:text="${userRoleOpt.displayName}"
></option>
</select>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input
type="password"
th:field="*{password}"
id="password"
class="form-control"
/>
<div
th:if="${#fields.hasErrors('password')}"
th:errors="*{password}"
class="invalid-feedback"
></div>
</div>
<div class="mb-3">
<label for="passwordConfirm" class="form-label"
>Пароль (подтверждение)</label
>
<input
type="password"
th:field="*{passwordConfirm}"
id="passwordConfirm"
class="form-control"
/>
<div
th:if="${#fields.hasErrors('passwordConfirm')}"
th:errors="*{passwordConfirm}"
class="invalid-feedback"
></div>
</div>
<div class="mb-3">
<label for="name" class="form-label">Имя</label>
<input
type="text"
th:field="*{name}"
id="name"
class="form-control"
/>
</div>
<div class="mb-3">
<label for="surname" class="form-label">Фамилия</label>
<input
type="text"
th:field="*{surname}"
id="surname"
class="form-control"
/>
</div>
<div class="mb-3">
<label for="birthday" class="form-label">Дата рождения</label>
<input
type="date"
th:field="*{birthday}"
id="birthday"
class="form-control"
/>
</div>
<div class="mb-3">
<label for="city" class="form-label">Место проживания</label>
<input
type="text"
th:field="*{city}"
id="city"
class="form-control"
/>
</div>
<div class="mb-3">
<label for="status" class="form-label">Статус</label>
<input
type="text"
th:field="*{status}"
id="status"
class="form-control"
/>
</div>
<button class="btn btn-success me-3" type="submit">Применить</button>
<a th:href="@{/admin/users(page=${page})}" class="btn btn-danger"
>Отмена</a
>
</form>
</main>
</body>
</html>

View File

@ -12,7 +12,7 @@
<th:block th:case="*">
<h2>Пользователи</h2>
<div>
<a th:href="@{/admin/users/create}" class="btn btn-primary">Добавить пользователя</a>
<a th:href="@{/admin/users/create}" class="btn btn-primary">Создать пользователя</a>
</div>
<table class="table">
<caption></caption>

View File

@ -1,13 +1,50 @@
<!DOCTYPE html>
<html lang="ru" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{index}">
<html
lang="ru"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{index}"
>
<head>
<title>Ноаости</title>
</head>
<head>
<title>Ноаости</title>
</head>
<body>
<main layout:fragment="content">
</main>
</body>
</html>
<body>
<main layout:fragment="content">
<div class="container d-flex flex-column w-50 flex-fill">
<form
sec:authorize="isAuthenticated()"
method="post"
th:action="@{/posts/create/{username}(username=${#authentication.name})}"
class="d-flex flex-column mb-3 mt-2"
>
<textarea
name="postBody"
class="mb-3"
placeholder="Напишите что-нибудь..."
></textarea>
<button type="submit" class="btn btn-primary">Опубликовать</button>
</form>
<th:block th:switch="${posts.size()}">
<th:block th:case="0">
<div class="h-100 d-flex align-items-center justify-content-center">
Здесь пока нет постов
</div>
</th:block>
<th:block th:case="*">
<th:block th:each="post : ${posts}">
<div th:replace="~{ posts :: post (post=${post})}"></div>
</th:block>
</th:block>
<th:block
th:replace="~{ pagination :: pagination (
url=${'admin/users'},
totalPages=${totalPages},
currentPage=${currentPage}) }"
/>
</th:block>
</div>
</main>
</body>
</html>

View File

@ -1,72 +1,126 @@
<!DOCTYPE html>
<html lang="ru" data-bs-theme="dark" xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
<html
lang="ru"
data-bs-theme="dark"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity6"
>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">
Неконтакте
</title>
<script
type="text/javascript"
src="/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js"
></script>
<link
rel="stylesheet"
href="/webjars/bootstrap/5.3.3/dist/css/bootstrap.min.css"
/>
<link
rel="stylesheet"
href="/webjars/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css"
/>
<link rel="stylesheet" href="/css/style.css" />
<meta
http-equiv="Cache-Control"
content="no-store, no-cache, must-revalidate"
/>
<meta http-equiv="Pragma" content="no-cache" />
<meta http-equiv="Expires" content="0" />
</head>
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/img/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title layout:title-pattern="$LAYOUT_TITLE - $CONTENT_TITLE">Неконтакте</title>
<script type="text/javascript" src="/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<link rel="stylesheet" href="/webjars/bootstrap/5.3.3/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="/webjars/bootstrap-icons/1.11.3/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="/css/style.css" />
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<meta http-equiv="Expires" content="0">
</head>
<body class="h-100 d-flex flex-column">
<nav class="navbar navbar-expand-md my-navbar" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center" href="/">
<img src="/img/favicon.svg" class="d-inline-block align-top me-4 logo" />
Неконтакте
</a>
<th:block sec:authorize="isAuthenticated()" th:with="userName=${#authentication.name}">
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-navbar"
aria-controls="main-navbar" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="main-navbar">
<ul class="navbar-nav me-auto link" th:with="activeLink=${#objects.nullSafe(servletPath, '')}">
<th:block sec:authorize="hasRole('ADMIN')">
<a class="nav-link" href="/admin/users" th:classappend="${activeLink.startsWith('/admin/users') ? 'active' : ''}">
Пользователи
</a>
<a class="nav-link" href="/h2-console/" target="_blank">Консоль H2</a>
</th:block>
<th:block sec:authorize="hasRole('USER')">
<a class="nav-link" href="/feed" th:classappend="${activeLink.startsWith('/feed') ? 'active' : ''}">
Новости
</a>
<a class="nav-link" href="/subs" th:classappend="${activeLink.startsWith('/subs') ? 'active' : ''}">
Подписки
</a>
<a class="nav-link"
th:href="@{/me}"
th:classappend="${activeLink.startsWith('/me') ? 'active' : ''}">
Профиль
</a>
</th:block>
</ul>
<ul class="navbar-nav" th:if="${not #strings.isEmpty(userName)}">
<form th:action="@{/logout}" method="post">
<button type="submit" class="navbar-brand nav-link" onclick="return confirm('Вы уверены?')">
Выход ([[${userName}]])
</button>
</form>
</ul>
</div>
</th:block>
</div>
</nav>
<main class="container w-50 p-2 d-flex flex-column flex-fill" layout:fragment="content">
</main>
<footer class="my-footer mt-auto d-flex flex-shrink-0 justify-content-center align-items-center">
Потапов Н.С. ПИбд-21, [[${#dates.year(#dates.createNow())}]]
</footer>
</body>
</html>
<body class="h-100 d-flex flex-column">
<nav class="navbar navbar-expand-md my-navbar" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand d-flex align-items-center" href="/">
<img
src="/img/favicon.svg"
class="d-inline-block align-top me-4 logo"
/>
Неконтакте
</a>
<th:block
sec:authorize="isAuthenticated()"
th:with="userName=${#authentication.name}"
>
<button
class="navbar-toggler"
type="button"
data-bs-toggle="collapse"
data-bs-target="#main-navbar"
aria-controls="main-navbar"
aria-expanded="false"
aria-label="Toggle navigation"
>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="main-navbar">
<ul
class="navbar-nav me-auto link"
th:with="activeLink=${#objects.nullSafe(servletPath, '')}"
>
<a
class="nav-link"
href="/feed"
th:classappend="${activeLink.startsWith('/feed') ? 'active' : ''}"
>
Новости
</a>
<a
class="nav-link"
href="/subs"
th:classappend="${activeLink.startsWith('/subs') ? 'active' : ''}"
>
Подписки
</a>
<a
class="nav-link"
th:href="@{/me}"
th:classappend="${activeLink.startsWith('/me') ? 'active' : ''}"
>
Профиль
</a>
<th:block sec:authorize="hasRole('ADMIN')">
<a
class="nav-link"
href="/admin/users"
th:classappend="${activeLink.startsWith('/admin/users') ? 'active' : ''}"
>
Пользователи
</a>
<a class="nav-link" href="/h2-console/" target="_blank"
>Консоль H2</a
>
</th:block>
</ul>
<ul class="navbar-nav" th:if="${not #strings.isEmpty(userName)}">
<form th:action="@{/logout}" method="post">
<button
type="submit"
class="navbar-brand nav-link"
onclick="return confirm('Вы уверены?')"
>
Выход ([[${userName}]])
</button>
</form>
</ul>
</div>
</th:block>
</div>
</nav>
<main
class="container w-50 p-2 d-flex flex-column flex-fill"
layout:fragment="content"
></main>
<footer
class="my-footer mt-auto d-flex flex-shrink-0 justify-content-center align-items-center"
>
Потапов Н.С. ПИбд-21, [[${#dates.year(#dates.createNow())}]]
</footer>
</body>
</html>

View File

@ -0,0 +1,48 @@
<!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>
<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>
</html>